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上的网络调试助手连接不上。