C/C++网络编程入门
2023-12-18 21:57:48 # 技术

基于TCP

0x00 服务器端函数用法

下面各小节将按照TCP服务器端默认的函数调用顺序来介绍其用法。绝大部分的TCP服务器端都按照该顺序调用。

0x01 socket() 创建套接字

在进入正题之前,先简单介绍一下文件与 Socket。
在 Linux 系统中,Socket 也被认为是文件的一种,因此在网络数据传输过程中也会用到文件 I/O 相关的函数。在 Linux 系统中成功调用 open() 函数打开一个文件时会返回一个文件描述符(File Descriptor, fd)。文件描述符是系统分配给文件或套接字的整数,用于方便标记操作系统创建的文件或套接字。(文件描述符有时也被称为「文件句柄」,但「句柄」主要是 Windows 系统中的术语)。
文件和套接字一般经过创建过程才会被分配文件描述符,而有三种输入输出对象即使未经过特殊的创建过程,在程序开始运行后也会被自动分配文件描述符。这三种输入输出对象为:

文件描述符 输入输出对象
0 标准输入
1 标准输出
2 标准错误

因为 0、1、2 是分配给标准 I/O 的描述符,所以系统在创建文件和套接字时所获的的文件描述符是从 3 开始以由小到大的顺序编号的。

好了,下面开始进入正题。

1
2
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • 返回对象:成功时返回一个文件描述符,失败时返回-1;
  • domain:套接字中使用的协议簇(protocol family,PF)信息;
  • type:套接字所选用的数据传输类型;
  • protocol :计算机间通信所采用的协议;

下面介绍各个参数的可选择传递对象。

协议簇(Protocol Family,PF)

常用的互联网协议簇为 IPv4 互联网协议簇,该协议簇在头文件 sys/socket.h 中被定义为 PF_INET

套接字类型

套接字类型值得是套接字的数据传输方式,在IPv4协议簇中也存在多种数据传输方式,因此需要指定具体采用哪种数据传输方式。具体的传输方式有:

  • SOCK_STREAM :创建面向连接的数据流套接字,其特征为
    • 传输过程中数据不会丢失;
    • 按序传输数据;
    • 传输的数据不存在数据边界,收到数据并不意味着马上调用 read() 函数,read() 函数和 write() 函数的调用次数并无太大意义;
  • SOCK_DGRAM:创建面向消息的套接字,其特征为
    • 强调快速传输而非传输顺序;
    • 传输的数据有可能丢失也有可能损坏;
    • 传输的数据有数据边界,这意味着接受数据的次数应和传输次数相同,也即 read() 函数和 write() 函数的调用次数一致;
    • 限制每次传输的数据大小;

协议的选择

大多数情况下,通过前两个参数(协议簇和套接字类型)已经决定了所采用的传输协议,所以大部分情况下可以向第三个参数传递 0,除非遇到「同一协议簇中存在多个数据传输方式相同的协议」的情况。
参数 PF_INET 指 IPv4 网络协议簇,SOCK_STREAM 是面向连接的数据传输,满足这两个条件的协议只有 IPPROTO_TCP,因此可以用如下方式创建 TCP 套接字:

1
2
#include <sys/socket.h>
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

满足 IPv4 协议簇和面向消息数据传输方式的协议只有 IPPROTO_UDP,因此可以用如下方式创建 UDP 套接字:

1
2
#include <sys/socket.h>
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

0x02 bind()

创建套接字后需要使用 bind() 函数为其分配 IP 地址和端口号。IP 地址用于区分网络上的计算机,而端口号用于区分计算机程序中所创建的套接字。
这里的 IP 地址主要是指的常用的 IPv4 地址,标准的 IPv4 地址用 4 字节表示。端口号由 2 个字节表示,可分配的端口号范围为 0~65535,但 0~1023 是知名端口号,一般分配给特定的应用程序,所以应当分配此范围之外的值。
另外,虽然端口号不能重复,但 TCP 套接字和 UDP 套接字不会共用端口号,所以允许重复。
bind()函数负责为套接字分配地址信息:

1
2
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);
  • 返回值:成功时返回0,失败时返回-1。
  • sockfd:要分配地址信息(IP地址和端口号)的套接字文件描述符。
  • myaddr:存有地址信息的结构体变量地址。
  • addrlen:存有地址信息的结构体变量的长度。

下面介绍描述地址信息的结构体。

表示IPv4地址的结构体

可以看到bind()函数需要的存有地址信息的结构体类型为sockaddr,其定义如下:

1
2
3
4
struct sockaddr {
sa_family_t sin_family; // 地址族(Address Family)
char sa_data[14]; // 地址信息
}

此结构体类型成员 sa_data 保存的地址信息中需包含 IP 地址和端口号,剩余部分应填充 0,而这对于构造地址信息来说比较麻烦,因而有了表示 IPv4 地址的结构体类型 sockaddr_in,其定义如下:

1
2
3
4
5
6
struct sockaddr_in {
sa_family_t sin_family; // 地址族(Address Family)
uint16_t sin_port; // 16位TCP/UDP端口号
struct in_addr sin_addr; // 32位IP地址
char sin_zero[8]; // 不使用,用于与sockaddr进行地址对齐
}

该结构体中提到的另一个结构体 in_addr 的定义如下,它用来存放 32 位 IP 地址:

1
2
3
4
struct in_addr {
in_addr_t s_addr; // 32位IPv4地址
}

结构体 sockaddr_in 的成员分析:

  • sin_family :每种协议族适用的地址族不同,AF_INET 表示 IPv4 协议族,该协议族使用 4 字节地址族。
  • sin_port :保存 16 位端口号,以网络字节序保存(大端序,数据的高位字节放到低位地址)。
  • sin_addr :保存 32 位 IP 地址信息,也以网络字节许保存。
  • sin_zero :无特殊含义,只用为了使结构体 sockaddr_in 的大小与 sockaddr 结构体保持一致而插入的填充成员。

字符串IP地址转为网络字节序的整数型IP地址

sockaddr_in 中所保存的地址信息成员位 32 位整数,因此,为了分配 IP 地址,需要将其表示为 32 位整数型数据。
而对于IP地址的表示,我们熟悉的是「点分十进制表示法」(Dotted Decimal Notation),而非整数型数据表示法。幸运的是有两个函数会帮我们把字符串形式的IP地址换成32位整数型数据,并同时完成网络字节序的转换:

1
2
3
4
5
6
7
8
#include <arpa/inet.h>

// 成功时返回32位大端序整数型值,失败时返回INADDR_NONE
in_addr_t inet_addr(const char* string);

// 成功时返回1(true),失败时返回0(false)
int inet_aton(const char* string, struct in_addr* addr);

inet_addr() 函数不仅可以把 IP 地址转为 32 位整数型网络字节序,而且可以检测无效的 IP 地址。
inet_aton()函数与inet_addr()函数在功能上完全相同,也将字符串形式的IP地址转换为32位网络字节序整数型IP地址,只不过该函数使用了in_addr的结构体,且其使用频率更高。

网络地址初始化并与套接字绑定

结合前面内容,套接字创建过程中常见的网络地址信息初始化方法,及其与套接字的绑定过程示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13

int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建一个TCP套接字

struct sockaddr_in addr; // 声明一个网络地址类型
char* server_ip = "1.2.3.4"; // 声明IP地址字符串
char* server_port = "8080"; // 声明端口号字符串
memset(&addr, 0, sizeof(addr)); // 将结构体变量addr的所有成员初始化位0
addr.sin_family = AF_INET; // 指定地址族
addr.sin_addr.s_addr = inet_addr(server_ip); // 基于字符串的IP地址初始化
addr.sin_port = htons(atoi(server_port)); // 基于字符串的端口号初始化

// 将套接字与网络地址信息绑定
bind(tcp_socket, (struct sockaddr*)&addr, sizeof(addr));

0x03 listen() 等待连接(监听)

在调用 bind() 函数为套接字分配了网络地址后,接下来就要通过调用 listen() 函数进入「等待连接请求」状态。服务器端只有调用了 listen() 函数,客户端才能进入可发出连接请求的状态。
服务器处于「等待连接请求」状态是指在客户端发出「请求连接」之前,服务器一直处于等待状态。

1
2
3
#include <sys/socket.h>

int listen(int sock, int backlog);
  • 返回值:成功时返回0,失败时返回-1。
  • sock:希望进入「等待连接请求」状态的套接字文件描述符,传递的文件描述符所属套接字将称为服务器端套接字(监听套接字)。
  • backlog:连接请求等待队列的长度,表示最多允许进入等待队列的连接请求数量。

0x04 accept() 受理客户端连接请求

调用listen()函数后,若有新的连接请求,则按序受理。「受理请求」意味着进入「可接受数据」的状态,这种状态下需要创建一个新套接字来专门与客户端进行数据交换,accpet()函数会自动创建套接字并连接到发起请求的客户端。

1
2
3
#include <sys/socket.h>

int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);
  • 返回值:成功时返回创建的套接字文件描述符,失败时返回-1。
  • sock:服务器套接字(监听套接字)的文件描述符。
  • addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后会向该地址变量参数填充客户端的地址信息。
  • addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址,调用函数后向该变量填充客户端地址长度。

    0x05 read()/write()以及close()

    直接上代码示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    #include <stdio.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <ctype.h>
    #include <strings.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <pthread.h>
    #include <sys/wait.h>
    #include <stdlib.h>
    #include <stdint.h>

    int main() {
    int server_fd = -1;
    int client_fd = -1;
    char SERVER_IP[] = "101.76.220.10";
    uint16_t SERVER_PORT = 1234;
    // 创建服务器端的套接字
    server_fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

    struct sockaddr_in server_addr; // 服务器地址信息
    struct sockaddr_in client_addr; // 客户端地址信息

    server_addr.sin_family = AF_INET; // 设置协议簇
    if(!inet_aton(SERVER_IP, &server_addr.sin_addr)) { // 设置IP地址
    printf("ip error!\n");
    }
    server_addr.sin_port = htons(SERVER_PORT); // 设置端口号

    // 将服务器端套接字保定到指定的地址和端口
    bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    // 监听
    listen(server_fd, 10);

    // 接受客户端的连接
    socklen_t client_addr_size = sizeof(client_addr);
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_size);

    // 向套接字写数据
    char buffer[] = "Hello World!";
    write(client_fd, buffer, sizeof(buffer));

    // 关闭所有套接字
    close(client_fd);
    close(server_fd);
    }

0x10 客户端函数用法

客户端相比服务器端要简单的多,因为创建套接字和请求连接就是客户端的全部内容。
创建套接字的过程同服务器端一样,区别在于客户端套接字用于「请求连接」,也即创建客户端套接字后向服务器端发起连接请求。因此这里只介绍发起连接请求的方法。

0x11 connect()发起连接请求

服务器端调用listen()函数后创建「连接请求等待队列」,之后客户端即可请求连接,发起请求连接的函数如下:

1
2
3
#include <sys/socket.h>

int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);
  • 返回值:成功时返回 0,失败时返回 -1 。
  • sock:客户端套接字文件描述符。
  • servaddr:保存有目标服务器地址信息的变量地址值。
  • addrlen:第二个结构体参数servaddr的地址变量长度。

客户端调用connect()函数后,发生以下情况之一才会返回(完成函数调用):

  • 服务器端接受连接请求。
  • 发生断网等异常情况而中断连接请求。

需要注意两点:

  • 所谓的「接受连接」并不意味着服务器端调用 accept() 函数,实际情况是服务器端把连接请求信息记录到「等待连接请求队列」。因此 connect() 函数返回后并不立即进行数据交换。
  • 客户端套接字创建过程中并没有调用 bind() 函数进行分配 IP 地址和端口,客户端套接字的 IP 地址和端口在调用 connect() 函数时会自动分配。

0x12 read()/write()以及close()

客户端代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdint.h>

#include <iostream>

int main() {
int fd = -1;
char SERVER_IP[] = "101.76.220.10"; // 服务器IP地址信息
uint16_t SERVER_PORT = 1234; // 服务器监听端口

// 创建客户端套接字
fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if(fd == -1) {
std::cout<<"fail create socket!"<<std::endl;
return 0;
}
struct sockaddr_in server_addr; // 创建服务器端地址信息变量
server_addr.sin_family = AF_INET; // 设置协议簇
// server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 设置IP地址
if(!inet_aton(SERVER_IP, &server_addr.sin_addr)) { // 设置IP地址
std::cout<<"ip error!"<<std::endl;
return 0;
}

server_addr.sin_port = htons(SERVER_PORT); // 设置端口号(并转为网络字节序

// 客户端套接字连接到服务器端的套接字
if(connect(fd, (struct sockaddr*)&server_addr, sizeof(sockaddr)) == -1) {
std::cout<<"connect error"<<std::endl;
return 0;
}
// 从套接字中读取数据
char buffer[2024];
if(read(fd, buffer, sizeof(buffer)) != -1) {
std::cout<<buffer<<std::endl;
return 0;
} else {
std::cout<<"read error!"<<std::endl;
}
close(fd); // 关闭套接字
return 0;
}

基于UDP的服务器端/客户端

为了提供可靠的数据传输,TCP 在不可靠的 IP 层进行流控制,而 UDP 就缺少这种流控制。UDP 的主要作用就是根据端口号将传到主机的数据包交付给最终的 UDP 套接字。
没有流控制的 UDP 相比 TCP 通常更快,主要原因有以下两点:

  • 收发数据前后进行的连接设置及清除过程。
  • 收发数据过程中为保证可靠性而添加的流控制。

因此,当收发的数据量小但需要频繁连接时,UDP 比 TCP 更高效。
UDP 服务器端/客户端不像 TCP 那样在连接状态下交换数据,也即不需要调用 TCP 连接过程中调用的 listen()connect() 函数,UDP 中只有创建套接字的过程和数据交换过程。也因此,UDP 套接字不会保持连接状态,每次传输数据时都要添加目标信息。

0x00 基于UDP的数据I/O函数

下面介绍填写数据接收地址并输出数据时调用的UDP函数。

1
2
3
#include <sys/socket.h>
ssize_t sendto(int sock, void* buff, size_t nbytes, int flags,
struct sockaddr* to, socklen_t addrlen);
  • 返回值:成功时返回传输的字节数,失败时返回-1 。
  • sock:用于传输数据的UDP套接字文件描述符。
  • buff:保存待传输数据的缓冲地址值。
  • nbytes:待传输数据的长度,以字节为单位。
  • flags:可选项参数,若没有则传递0。
  • to:存有目标地址信息的sockaddr结构体变量的地址值。
  • addrlen :参数 to 的地址值结构体变量长度。

上述函数与之前的 TCP 输出函数最大的区别在于,此函数需要向它传递目标地址信息。接下来介绍接收 UDP 数据的函数。
UDP 数据的发送端并不固定,因此该函数定义为可接收「发送端信息」的形式,也即可以同时返回 UDP 数据包中的「发送端信息」。

1
2
3
4
#include <sys/socket.h>

ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
struct sockaddr* from, socklen_t *addrlen);
  • 返回值:成功时返回接收的字节数,失败时返回-1 。
  • buff:保存接收数据的缓冲地址值。
  • nbytes :可接受的最大字节数,故不能超过参数 buff 所指定的缓冲区大小。
  • flags:可选项参数,若没有则传入0 。
  • from :用于保存接收到的发送端地址信息的 sockaddr 结构体变量的地址值。
  • addrlen :用于保存参数 from 的结构体变量长度的变量地址值。

0x10 基于UDP的「回声」服务器端/客户端

UDP不同于TCP,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务器端和客户端。这里只是把提供服务的一端称为服务器端,把请求服务的一端称为客户端。

0x11 UDP服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 30

void error_handling(const char* message);

int main(int argc, char* argv[]) {
int serv_sock;
char message[BUF_SIZE];

int str_len;

struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;

if(argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}

serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(serv_sock == -1) {
error_handling("UDP socket creation error");
}

memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));

if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) {
error_handling("bind() error");
}

while(1) {
clnt_adr_sz = sizeof(clnt_adr);
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);
}
close(serv_sock);
return 0;
}

void error_handling(const char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

下面介绍于上述服务器端协同工作的客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 30

void error_handling(const char* message);

int main(int argc, char* argv[]) {
int sock;
char message[BUF_SIZE];

int str_len;

struct sockaddr_in serv_addr, from_addr;
socklen_t adr_sz;

if(argc != 3) {
printf("Usage : %s <IP> : <port>\n", argv[0]);
exit(1);
}

sock = socket(PF_INET, SOCK_DGRAM, 0);
if(sock == -1) {
error_handling("socket() error!");
}

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));

while(1) {
fputs("Please input message(Q to quit) : ", stdout);
fgets(message, sizeof(message), stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
break;
}

sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
adr_sz = sizeof(from_addr);
str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_addr, &adr_sz);
message[str_len] = 0;
printf("Message from server : %s", message);
}

close(sock);
return 0;

}

void error_handling(const char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

UDP 程序中,调用 sendto() 函数传输数据前应完成对套接字的地址分配工作,因此需要调用 bind() 函数,该函数不区分 TCP 和 UDP,也即在 UDP 和 TCP 程序中均可调用。
另外,如果调用 sendto() 函数时发现尚未分配地址信息,则在首次调用 sendto() 时给相应的套接字自动分配 IP 地址和端口。因此,UDP 客户端中通常无需额外的地址分配过程。

0x20 存在数据边界的UDP套接字

TCP 数据传输过程中不存在数据边界,这表示「数据传输过程中调用 I/O 函数的次数不具有任何意义」。
而 UDP 是具有数据边界的协议,数据传输过程中调用 I/O 函数的次数非常重要。因此,输入函数的调用次数应该和输出函数的调用次数完全一致,这样才能保证接收端能够接收到所有已发送数据。

UDP 套接字传输的数据包又称为「数据报」,实际上数据报也属于数据包的一种。UDP 中存在数据边界,一个数据包即可成为一个完整数据,因此称为数据报。

0x30 已连接UDP套接字和未连接UDP套接字

TCP 套接字中需要调用 connect() 函数注册待传输数据的目标 IP 和端口号,而 UDP 中则无需注册。因此,通过 sendto() 函数传输数据的过程大致可以分为以下 3 个阶段:

  • 第一阶段:向 UDP 套接字注册目标 IP 和端口号。
  • 第二阶段:传输数据。
  • 第三阶段:删除UDP套接字中注册的目标地址信息。

每次调用 sendto() 函数时都会重复上述过程,每次都变更目标地址,因此可以重复利用同一个 UDP 套接字向不同的目标地址传输数据。
这种未注册目标地址信息的套接字被称为未连接套接字,反之,注册了目标地址的套接字称为连接(connected)套接字。显然,UDP 套接字默认属于未连接套接字。
在上述三个阶段中,第一个和第三个阶段占用整个通信过程将近 1/3 的时间,缩短这部分时间则将大大提高整体性能。因此,若要与同一主机进行长时间通信,将 UDP 套接字变成已连接套接字将会大大提高效率。

0x31 创建已连接套接字

要创建已连接UDP套接字只需要对UDP套接字调用connect()函数。

1
2
3
4
5
6
7
8
9
sock = socket(PF_INET, SOCK_DGRAM, 0);

struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

针对 UDP 套接字调用 connect() 函数并不意味着要与对方 UDP 套接字连接,这只是向 UDP 套接字注册目标 IP 和端口信息。
注册之后就与 TCP 套接字一样,每次调用 sendto() 函数时只需传输数据。因为已经指定了收发对象,所以不仅可以使用 sendto()recvfrom() 函数,还可以使用 write()read() 函数进行通信。
可以对上文提到的 UDP 客户端程序进行修改,改为已连接 UDP 套接字,修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 30

void error_handling(const char* message);

int main(int argc, char* argv[]) {
int sock;
char message[BUF_SIZE];

int str_len;

struct sockaddr_in serv_addr, from_addr;
socklen_t adr_sz;

if(argc != 3) {
printf("Usage : %s <IP> : <port>\n", argv[0]);
exit(1);
}

sock = socket(PF_INET, SOCK_DGRAM, 0);
if(sock == -1) {
error_handling("socket() error!");
}

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));

// 向UDP套接字注册目标IP和端口信息
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

while(1) {
fputs("Please input message(Q to quit) : ", stdout);
fgets(message, sizeof(message), stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
break;
}

// sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// 注册目标IP和端口信息后可以使用write()和read()函数传输数据
write(sock, message, strlen(message));

adr_sz = sizeof(from_addr);
// str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_addr, &adr_sz);
read(sock, message, sizeof(message) - 1);
message[str_len] = 0;
printf("Message from server : %s", message);
}

close(sock);
return 0;

}

void error_handling(const char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}