Linux下编写TCP服务器调用的函数顺序为:socket -> bind -> listen -> accept -> recv/send
socket
参见:http://c.biancheng.net/view/2131.html
socket函数成功返回文件描述符,失败返回-1
bind
参见:http://c.biancheng.net/view/2344.html
需要注意的是,bind函数的第二个参数类型为struct sockaddr *,但用的时候经常传入struct sockaddr_in *类型的参数,具体原因参考网页中有说。并且如果使用的结构是struct sockaddr_in,那么需要包含netinet/in.h。
struct sockaddr_in结构体中的成员变量参考网页中也有说,需要注意其中的sin_port成员表示端口,需要用htons函数转换,htons函数说明如下:https://blog.csdn.net/zhuguorong11/article/details/52300680
sin_addr.s_addr也需要用inet_addr函数转换,需要注意的是,给sin_addr.s_addr赋值为0表示绑定本机IP,这也是常用的写法。
第三个参数类型为socklen_t,直接传入sizeof(struct sockaddr_in)即可。
返回值:成功返回0,失败返回-1。
如果程序在acctpt之后非正常退出(ctrl+c),下次bind可能会失败,需要等待一段时间恢复。
listen和accept
参见:http://c.biancheng.net/view/2345.html
listen函数成功返回0,失败返回-1
而accept成功返回一个文件描述符,失败返回-1,这个文件描述符就是可以用recv和send函数通信的文件描述符。
accept函数默认会阻塞,直到有客户端来连接。
accept函数的第2个参数是输出参数,用来得到客户端的地址和端口
第三个参数是输入参数,指定的是第二个参数占用的空间大小,即sizeof(struct sockaddr_in)。
特别注意第三个参数是输入参数而不是输出参数,我在一次测试就遇到了这个问题,我在想如果第三个参数如果是输入,那为什么要传指针类型,因此我将第三个参数指向的值设为0,结果accept运行返回OK了,并且打印第三个参数的值也变成了16。但是打印客户端的IP地址却变成了0.0.0.0,端口号也是0。因此在accept的时候传入的第三个参数指向的值应为sizeof(struct sockaddr_in)。
accept执行成功后可以用inet_ntoa函数将IP地址以字符串的方式取出, inet_ntoa的函数原型为:char *inet_ntoa(struct in_addr in);
有一点要特别注意,如果使用了inet_ntoa函数,那么就必须包含arpa/inet.h头文件,否则当你用一个char *类型的变量去接收inet_ntoa函数的返回值时会报一个很奇怪的警告,意思就是inet_ntoa函数返回的是一个int型,但是我用man手册反反复复看了不下5遍,inet_ntoa函数返回的就是char *类型,最后网上找资料发现是必须要包含arpa/inet.h头文件。不过有趣的是,如果我不包含arpa/inet.h头文件,而就用一个int类型的变量来接收inet_ntoa的返回值,结果编译还不会报任何警告和错误,运行程序时还真的能得到一个int类型的值。
用ntohs取出端口号,ntohs的函数原型为uint16_t ntohs(uint16_t netshort);
recv和send
recv用于接收数据,函数原型为:ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:套接字描述符,注意该描述符不是socket函数返回的描述符,而是accept函数返回的描述符。
buf:用于存放数据的缓冲区
len:希望接收的最大字节个数
flags:一般设置为0,具体描述在man手册中可以看到
send用于发送数据,函数原型为:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:套接字描述符,注意该描述符不是socket函数返回的描述符,而是accept函数返回的描述符。
buf:用于存放数据的缓冲区
len:希望发送的最大字节个数
flags:一般设置为0,具体描述在man手册中可以看到
如果客户端断开连接,那么recv函数将不阻塞,返回值为0,可以通过这个方法判断客户端是否断开连接。
接收数据可以用read函数代替,发送数据也可以用send函数代替。
例程:
tcp_server.c
1 /**
2 * filename: tcp_server.c
3 * author: Suzkfly
4 * date: 2021-01-22
5 * platform: Ubuntu
6 * 配合windows的网络调试工具使用:
7 * 1、先保证windows与Ubuntu在同一网段且互相能ping通;
8 * 2、在windows下打开网络调试助手,选择协议类型为TCP Client,远程主机地址为
9 * Ubuntu的IP地址,远程主机端口为Ubuntu例程中写的端口,接收设置和发送设
10 * 置都选择ASCLL。
11 * 3、运行Ubuntu下的TCP服务器程序;
12 * 4、网络调试助手上点击“连接”。
13 * 5、连接成功后在网络调试助手上发送数据,在Ubuntu下的终端上能看到,
14 * 在Ubuntu下的终端上输入字符串按回车发送,在windows上的网络调试助手上也
15 * 能看到。
16 */
17 #include <stdio.h>
18 #include <sys/types.h>
19 #include <sys/socket.h>
20 #include <string.h>
21 #include <netinet/in.h>
22 #include <arpa/inet.h>
23 #include <errno.h>
24
25 //#define IP_ADDR "127.0.0.1" /* IP地址 */
26 #define PORT 10000 /* 端口号 */
27
28 int main(int argc, const char *argv[])
29 {
30 int sock_fd = 0, confd = 0;
31 struct sockaddr_in serv_addr; /* 服务器IP(本机IP) */
32 struct sockaddr_in client_addr; /* 客户端IP(连接者IP) */
33 socklen_t addr_len = sizeof(struct sockaddr_in);
34 int ret = 0; /* 用于接收函数返回值 */
35 int pid = 0;
36 char buf[128] = { 0 }; /* 用于存放数据的缓冲区 */
37 int len = 0; /* 发送和接收数据的长度 */
38
39 /* 创建TCP套接字 */
40 sock_fd = socket(AF_INET, SOCK_STREAM, 0);
41
42 /* 将套接字与IP和端口绑定 */
43 memset(&serv_addr, 0, sizeof(struct sockaddr_in));
44 serv_addr.sin_family = AF_INET;
45 //serv_addr.sin_addr.s_addr = inet_addr(IP_ADDR); /* 绑定IP */
46 serv_addr.sin_addr.s_addr = 0; /* 绑定0就是绑定自己 */
47 serv_addr.sin_port = htons(PORT); /* 端口号 */
48 ret = bind(sock_fd, (struct sockaddr*)&serv_addr, sizeof(struct sockaddr_in));
49 if (ret == 0) {
50 printf("bind ok\n");
51 } else {
52 printf("bind failed\n");
53 close(sock_fd);
54 return 0;
55 }
56
57 /* 让套接字进入被动监听状态 */
58 ret = listen(sock_fd, 10);
59 if (ret == 0) {
60 printf("listen ok\n");
61 } else {
62 printf("listen failed\n");
63 close(sock_fd);
64 return 0;
65 }
66
67 re_connect:
68
69 /* 接收客户端请求(阻塞) */
70 memset(&client_addr, 0, sizeof(client_addr));
71 printf("accept...\n");
72 confd = accept(sock_fd, (struct sockaddr *)&client_addr, &addr_len);
73 if (confd > 0) {
74 printf("accept ok\n");
75 } else {
76 printf("accept failed\n");
77 close(sock_fd);
78 return 0;
79 }
80
81 /* 打印客户端信息 */
82 printf("addr_len = %d\n", addr_len);
83 printf("Client IP: %s\n", inet_ntoa(client_addr.sin_addr)); /* IP地址 */
84 printf("Client Port:%d\n", ntohs(client_addr.sin_port)); /* 端口号 */
85
86 pid = fork();
87
88 if (pid > 0) { /* 接收数据 */
89 while (1) {
90 memset(buf, 0, sizeof(buf));
91 len = recv(confd, buf, sizeof(buf), 0);
92 //len = read(confd, buf, sizeof(buf));
93 if (len == 0) { /* 如果recv返回0,则表示远端断开连接 */
94 goto re_connect;
95 }
96 printf("len = %d\n", len);
97 printf("data: %s\n", buf);
98 }
99 } else if (pid == 0) {
100 while (1) { /* 发送数据 */
101 memset(buf, 0, sizeof(buf));
102 scanf("%s", buf);
103 len = send(confd, buf, strlen(buf), 0);
104 //len = write(confd, buf, strlen(buf));
105 }
106 }
107 }
该测试程序有一个BUG,如果客户端断开连接,那么recv返回0,则会跳转到代码第67行重新连接,如果连接成功,则在第86行又会调用fork函数,创建出一个新的进程。在测试时发现,如果客户端断开连接并重新连接后的一端时间内,在服务器终端中输入的数据在网络调试助手中收不到。这个问题在https://www.cnblogs.com/Suzkfly/p/14326811.html这篇博客中解决了。
网络调试助手设置如下:
注意:
例程中如果使用windows的网络调试工具,如果在绑定的时候指定回环网卡,则在windows上的网络调试助手连接不上。