关键词搜索

源码搜索 ×
×

C 语言网络编程 — Socket I/O 性能优化手段

发布2023-04-03浏览4433次

详情内容

目录

Socket I/O 处理流程

每次 Socket I/O 操作,大体上都需要经历 2 个阶段:

  1. 准备数据阶段:数据包到达 Kernel 并就绪,Application 可以开始数据拷贝。
  2. 拷贝数据阶段:Application 通过 SCI 将数据从 Kernel 拷贝到 Userspace(进程虚拟地址空间)中。

对于上述 I/O 流程,基于 BSD Socket API 可以实现以下几种 I/O 模式:

  1. 阻塞式 I/O(Blocking I/O)
  2. 非阻塞式 I/O(Non-Blocking I/O)
  3. I/O 多路复用(I/O Multiplexing)

从阻塞程度的角度出发,效率由低到高为:阻塞 IO > 非阻塞 IO > 多路复用 IO。

阻塞式 I/O(Blocking I/O)

阻塞:是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务。

默认情况下的 Socket I/O 都是阻塞式 I/O。

请添加图片描述

如下图所示:

  1. 准备数据阶段:当 Application 调用 recvfrom() 后,Kernel 开始准备数据,需等待足够多的数据再进行拷贝。而在等待 “数据就绪” 的过程中,Application 会被阻塞;
  2. 拷贝数据阶段:数据就绪后,Kernel 开始拷贝数据,将数据复制到 Application Buffer。拷贝完成后,Application 才会从阻塞态进入到就绪态。

可见,阻塞式 I/O 模式,Application 在调用 recvfrom() 开始,直到从 recvfrom() 返回的这段时间里,Application 都是被阻塞的。

阻塞式 I/O 模式的缺点

  1. 当并发较大时,需要创建大量的线程来处理连接,占用了大量的系统资源。
  2. TCP connection 建立完成后,如果当前线程没有数据可读,将会阻塞在 recvfrom() 操作上,造成线程资源的浪费。

在这里插入图片描述

非阻塞式 I/O(Non-Blocking I/O)

非阻塞:是指函数不会因为等待数据就绪而阻塞当前线程,而是会立刻返回。

可以使用 fcntl() 函数显式地为 socket fd 设置 O_NONBLOCK 标志,以此来启动非阻塞式 I/O 模式的 Socket API。

  1. 数据准备阶段

    1. Application 调用 read() 后,如果 Kernel BSD Socket Buffer 中的数据还没有准备好(或者没有任务数据),那么 non-blocking Socket 立刻返回一个 EWOULDBLOCK Error 给 Application。
    2. Application 接收到 EWOULDBLOCK Error 后,得知 “数据未就绪“,Application 不被阻塞,继续执行其他任务,并且等待一段时候之后,再次调用 read()。
  2. 数据拷贝阶段:直到数据就绪后,正常进行数据拷贝,然后返回。

可见,非阻塞 I/O 模式,Application 通过不断地询问 Kernel 数据是否就绪,以此来规避了 “空等"。

非阻塞式 I/O 模式的缺点

  1. Application 使用 non-blocking Socket 时,会不停的 Polling(轮询)Kernel,以此检查是否 I/O 操作已经就绪。这极大的浪费了 CPU 的资源。
  2. 另外,非阻塞 IO 需要 Application 多次发起 read(),频繁的 SCI(系统调用)也是比较消耗系统资源的。

在这里插入图片描述

阻塞 IO 与非阻塞 IO 的区别
在这里插入图片描述

I/O 多路复用(I/O Multiplexing)

多路复用(Multiplexing)是一个通信领域的术语,指在同一个信道上传输多路信号或数据流的过程和技术。

而在 Socket I/O 场景中的多路复用,即:使用同一个 Server Socket fd(信道)处理多个 Client Socket fds 的 I/O 请求,以此来避免单一连接的阻塞,进而提升 Application 的处理性能。
在这里插入图片描述

值得注意的是,I/O 多路复用的本质是一种 “设备 I/O” 技术,常用于 “网络 I/O” 场景,当然也可以用于其他 I/O 场景。

Linux Kernel 提供了 3 个用于支持 I/O 多路复用的设备 I/O 接口,包括:select()、poll() 和 epoll()。其中又以 epoll() 的性能最佳,所以后面会着重介绍这一方式。

在这里插入图片描述

select()

函数声明

  • n 参数:读、写、异常集合中的 fds(文件描述符)数量的最大值 +1。
  • readfds 参数:读 fds 集合。
  • writefds 参数:写 fds 集合。
  • exceptfds 参数:异常 fds 集合。
  • timeout 参数:超时设置。
  • int 函数返回值:返回监听到的数据就绪的 fds 的标记。
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

    select() 的缺点

    1. 单一 Application 能监视的 Socket fds 数量有限制,一般为 1024 个,需要通过调整 Kernel 参数进行修改。
    2. 每次调用 select() 时都需要将所有的 Socket fds Set 从 Userspace 复制到 Kernel space,造成性能开销。
    3. Kernel 根据实际情况为所有的 Socket fds Set 逐一打上可读、可写或异常的标记,然后返回。Application 需要循环遍历这些 fds 的标记,然后逐一进行相应的处理。

    在这里插入图片描述

    poll()

    函数声明

    • fds 参数:struct pollfd 类型指针,用于指定需要监视的 fds 以及 Application 所关心的 Events。
    • nfds 参数:指示 fds 的数量。
    • timeout 参数:超时配置,单位是毫秒。为负数时,表示无限期等待,直到有事件发生。
    #include <poll.h>
    
    int poll(struct pollfd *fds, unsigned int nfds, int timeout);
    
      2
    • 3

    poll() 相较于 select() 的改进

    1. 没有了 Socket fds 监听数量的限制。
    2. 引入了事件监听机制,支持为每个 Socket fd 设置监听事件。

    pollfd 结构体包含了要监视的 events 和已经发生了的 revents。当 Application 关心的事件发生时,poll() 才会返回,然后 Application 可以开始执行相应的操作。

    struct pollfd {
        int fd;         // 监视的文件描述符
        short events;   // 文件描述符所关心的事件
        short revents;  // 实际发生的事件
    };
    
      2
    • 3
    • 4
    • 5

    poll() 的缺点

    1. 依旧需要在 Userspace 和 Kernel space 之间传递大量的 Socket fds Set,造成性能损耗。
    2. 依旧需要 Application 遍历所有的 Socket fds,处理效率不高。

    epoll

    为了彻底解决 select() 和 poll() 的不足,epoll() 设计了全新的运行模式,通过下面两张图片来直观的进行比较。

    • select() 运行原理

      • 每次调用 select() 都需要在 User space 和 Kernel space 之间传递 Socket fds set。
      • Application 需要对 Kernel 返回的 Socket fds Set 进行轮询遍历处理。
        请添加图片描述
    • epoll() 运行原理

      • 首先调用 epoll_create(),在 Kernel space 开辟一段内存空间,用来存放所有的 Socket fds Set,以此避免了模式转换的数据复制。
      • 然后调用 epoll_ctrll(),根据需要逐一的为 Socket fd 绑定监听 events 并添加到这块空间中。
      • 最后调用 epoll_wait(), 等待 events 发生,并进行异步回调,使用 “回调监听” 代替 “轮询监听”,以此提升了性能的同时降低的损耗。
        请添加图片描述

    epoll 的工作模式

    epoll 提供了 2 种工作模式,可以根据实际需要进行设置:

    1. LT(Level Trigger,水平触发)模式:默认的工作模式,当 epoll_wait 监听到有 Socket fd 就绪时,就将相应的 Event 返回给 Application。此时 Application 可以根据具体的情况选择立即处理或延后处理该 Event。如果不处理,那么待下次调用 epoll_wait 时,epoll 还会再次返回此 Event。同时支持阻塞 I/O 和非阻塞 I/O 模式。

    2. ET(Edge Trigger,边缘触发)模式:相对的,ET 模式则不会再次返回 Missed 的 Event。避免了 epoll events 被重复触发的情况,因此 ET 的处理效率要比 LT 高,常应用于高压场景。只支持非阻塞 I/O 模式。

    epoll_create()

    函数作用:创建一个 epoll 实例。
    函数原型

    • size 参数:指示 Kernel 需要监听的 Socket fds 的数目。
    • 函数返回值
      • 成功:返回一个 epoll fd(句柄)。
    int epoll_create(int size)

      epoll_ctl()

      函数作用:对指定 epoll fd 执行相应的操作。

      函数原型

      • epfd 参数:指示一个 epoll fd。
      • op 参数:指示操作的类型,可选:
        • EPOLL_CTL_ADD:添加 Socket fd 及其监听事件。
        • EPOLL_CTL_DEL:删除 Socket fd 及其监听事件。
        • EPOLL_CTL_MOD:修改 Socket fd 的监听事件。
      • fd 参数:指示需要监听的 Socket fd。
      • epoll_event 参数:指示 Kernel 需要监听 Socket fd 的事件。
      int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

        epoll_event 数据结构如下:

        struct epoll_event {
        	__uint32_t events;  /* Epoll events */ 
        	epoll_data_t data;  /* User data variable */
        };
        
          2
        • 3
        • 4

        events 可选:

        • EPOLLIN :表示 Socket fd 可读事件(包括对端 Socket 正常关闭);
        • EPOLLOUT:表示 Socket fd 可写事件;
        • EPOLLPRI:表示 Socket fd 有紧急的数据可读事件(这里应该表示有带外数据到来);
        • EPOLLERR:表示 Socket fd 发生异常事件;
        • EPOLLHUP:表示 Socket fd 被挂断事件(对端已经关闭连接或者关闭了写操作);
        • EPOLLET: 表示将 epoll fd 设为 ET 模式。
        • EPOLLONESHOT:表示只监听一次事件。如果后续还需要监听这个 Socket fd 的话,需要重新加入到 epoll 队列里。

        epoll_wait()

        函数功能:等待 epoll fd 的 I/O 事件,一次最多可返回 maxevents 个事件。
        函数原型

        • epfd 参数:指示一个 epoll fd。
        • events 参数:指示 Kernel 返回的 Events 的容器。
        • maxevents 参数:指示 events 参数的数量,不能大于 epoll_create() 时指定的 size。
        • timeout 参数:超时时间,单位为毫秒,0 表示立即返回,-1 表示永久阻塞。
        • 函数返回值:返回需要处理的事件数目,0 表示已超时。
        int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
        

          基于 epoll 和非阻塞 I/O 的 TCP Socket 优化示例

          服务端

          #include <stdio.h>
          #include <stdlib.h>
          #include <string.h>
          #include <unistd.h>
          #include <errno.h>
          #include <fcntl.h>
          
          #include <arpa/inet.h>
          #include <sys/socket.h>
          #include <sys/epoll.h>
          
          
          #define ERR_MSG(err_code) do {                                     \
              err_code = errno;                                              \
              fprintf(stderr, "ERROR code: %d \n", err_code);                \
              perror("PERROR message");                                      \
          } while (0)
          
          
          #define MAX_EVENTS 10
          #define BUFFER_SIZE 1024
          
          
          /* 设置 Socket 为非阻塞 I/O 模式。*/
          static int set_sock_non_blocking(int sock_fd)
          {
              int flags, s;
          
              flags = fcntl(sock_fd, F_GETFL, 0);
              if (flags == -1)
              {
                  perror ("fcntl");
                  return -1;
              }
          
              flags |= O_NONBLOCK;
              s = fcntl(sock_fd, F_SETFL, flags);
              if (s == -1)
              {
                  perror ("fcntl");
                  return -1;
              }
          
              return 0;
          }
          
          
          int main(void)
          {
              /* 配置 Server Sock 信息。*/
              struct sockaddr_in srv_sock_addr;
              memset(&srv_sock_addr, 0, sizeof(srv_sock_addr));
              srv_sock_addr.sin_family = AF_INET;
              srv_sock_addr.sin_addr.s_addr = htonl(INADDR_ANY);
              srv_sock_addr.sin_port = htons(8086);
          
              /* 创建 Server Socket fd 实例。*/
              int srv_socket_fd = 0;
              if (-1 == (srv_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))) {
                  printf("Create socket file descriptor ERROR.\n");
                  ERR_MSG(errno);
                  exit(EXIT_FAILURE);
              }
          
              /* 设置 Server Socket fd 选项。*/
              int optval = 1;
              if (setsockopt(srv_socket_fd,
                             SOL_SOCKET,    // 表示套接字选项的协议层。
                             SO_REUSEADDR,  // 表示在绑定地址时允许重用本地地址。这样做的好处是,当服务器进程崩溃或被关闭时,可以更快地重新启动服务器,而不必等待一段时间来释放之前使用的套接字。
                             &optval,
                             sizeof(optval)) < 0)
              {
                  printf("Set socket options ERROR.\n");
                  ERR_MSG(errno);
                  exit(EXIT_FAILURE);
              }
          
              /* 绑定 Server Socket fd 与 Sock Address 信息。*/
              if (-1 == bind(srv_socket_fd,
                             (struct sockaddr *)&srv_sock_addr,
                             sizeof(srv_sock_addr)))
              {
                  printf("Bind socket ERROR.\n");
                  ERR_MSG(errno);
                  exit(EXIT_FAILURE);
              }
          
              /* Server Socket fd 开始监听 Client 发出的连接请求。*/
              if (-1 == listen(srv_socket_fd, 10))
              {
                  printf("Listen socket ERROR.\n");
                  ERR_MSG(errno);
                  exit(EXIT_FAILURE);
              }
          
              /* 设置 Server Socket fd 为非阻塞模式。*/
              if (-1 == set_sock_non_blocking(srv_socket_fd))
              {
                  printf("set_sock_non_blocking() error.");
                  ERR_MSG(errno);
                  close(srv_socket_fd);
              }
          
              /* 创建一个 epoll 实例。*/
              int epoll_fd = -1;
              if (-1 == (epoll_fd = epoll_create1(0)))
              {
                  printf("epoll_create error.");
                  ERR_MSG(errno);
                  exit(EXIT_FAILURE);
              }
          
              /* 定义 epoll ctrl event 和 callback events 实例 */
              struct epoll_event event, events[MAX_EVENTS];
              /* 将 Server Socket fd 添加到 epoll 实例的监听列表中 */
              event.data.fd = srv_socket_fd;
              /* 设置 epoll 的 Events 类型为 EPOLLIN(可读事件)和 EPOLLET(采用 ET 模式)。*/
              event.events = EPOLLIN | EPOLLET;
              /* 将 Server Socket fd 添加(EPOLL_CTL_ADD)到 epoll 实例的监听列表中,并设定监听事件类型。*/
              if (-1 == epoll_ctl(epoll_fd, EPOLL_CTL_ADD, srv_socket_fd, &event))
              {
                  printf("epoll_ctl error.");
                  ERR_MSG(errno);
                  exit(EXIT_FAILURE);
              }
          
              printf("Starting TCP server.\n");
              int i, event_cnt;
              while (1)
              {
                  /* epoll 实例开始等待事件,一次最多可返回 MAX_EVENTS 个事件,并存放到 events 容器中。*/
                  if (-1 == (event_cnt = epoll_wait(epoll_fd, events, MAX_EVENTS, -1)))
                  {
                      printf("epoll_wait error.");
                      ERR_MSG(errno);
                      exit(EXIT_FAILURE);
                  }
          
                  for (i = 0; i < event_cnt; ++i)
                  {
                      /* Server Socket fd 有可读事件,表示有 Client 发起了连接请求。*/
                      if (srv_socket_fd == events[i].data.fd)
                      {
                          printf("Accepted client connection request.\n");
                          for ( ;; )
                          {
                              /* 初始化 Client Sock 信息存储变量。*/
                              struct sockaddr cli_sock_addr;
                              memset(&cli_sock_addr, 0, sizeof(cli_sock_addr));
                              int cli_sockaddr_len = sizeof(cli_sock_addr);
          
                              int cli_socket_fd = 0;                
                              if (-1 == (cli_socket_fd = accept(srv_socket_fd,
                                                                (struct sockaddr *)(&cli_sock_addr),  // 填充 Client Sock 信息。
                                                                (socklen_t *)&cli_sockaddr_len)))
                              {
                                  /* 如果是 EAGAIN(Try again )错误或非阻塞 I/O 的 EWOULDBLOCK(Operation would block)错误通知,则直接 break,继续循环,直到 “数据就绪” 为止。*/
                                  if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
                                      break;
                                  } else {
                                      printf("Accept connection from client ERROR.\n");
                                      break;
                                  }                    
                              }     
          
                              /* 设置 Client Socket 为非阻塞模式。*/
                              if (-1 == set_sock_non_blocking(cli_socket_fd))
                              {
                                  printf("set_sock_non_blocking() error.");
                                  close(cli_socket_fd);
                                  break;
                              }
          
                              /* 将 Client Socket fd 添加到 epoll 实例的监听列表中 */
                              event.data.fd = cli_socket_fd;
                              event.events = EPOLLIN | EPOLLET;  // 设定可读监听事件,并采用 ET 模式。
                              if (-1 == epoll_ctl(epoll_fd, EPOLL_CTL_ADD, cli_socket_fd, &event))  // 添加 Client Socket fd 及其监听事件。
                              {
                                  printf("epoll_ctl() error.");
                                  close(cli_socket_fd);
                                  break;
                              }
                          }
                      }
                      /* 发生了数据等待读取事件。因为 epoll 实例正在使用 ET 模式,所以必须完全读取所有可用数据,否则不会再次收到相同数据的通知。*/
                      else if (events[i].events & EPOLLIN)
                      {
                          printf("Received client sent data.\n");
          
                          int cli_socket_fd = events[i].data.fd;
                          char buff[BUFFER_SIZE];
                          int recv_len = 0;
                          /* 接收指定 Client Socket 发出的数据,*/
                          if ((recv_len = recv(cli_socket_fd, buff, BUFFER_SIZE, 0)) < 0)
                          {
                              printf("Receive from client ERROR.\n");
                              close(cli_socket_fd);
                              break;
                          }
                          printf("Recevice data from client: %s\n", buff);
          
                          /* 将收到的数据重新发送给指定的 Client Socket。*/
                          send(cli_socket_fd, buff, recv_len, 0);
                          printf("Send data to client: %s\n", buff);
          
                          /* 每处理完一次 Client 请求,即关闭连接。*/
                          close(cli_socket_fd);
                          memset(buff, 0, BUFFER_SIZE);
                      }
                      /* 发生了 epoll 异常事件,直接关闭 Client Socket fd。*/
                      else if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN))) {
                          printf("epoll error.");
                          close(events[i].data.fd);
                          break;
                      }
                  }
              }
          
              close(srv_socket_fd);
              return EXIT_SUCCESS;
          }
          
            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
          • 68
          • 69
          • 70
          • 71
          • 72
          • 73
          • 74
          • 75
          • 76
          • 77
          • 78
          • 79
          • 80
          • 81
          • 82
          • 83
          • 84
          • 85
          • 86
          • 87
          • 88
          • 89
          • 90
          • 91
          • 92
          • 93
          • 94
          • 95
          • 96
          • 97
          • 98
          • 99
          • 100
          • 101
          • 102
          • 103
          • 104
          • 105
          • 106
          • 107
          • 108
          • 109
          • 110
          • 111
          • 112
          • 113
          • 114
          • 115
          • 116
          • 117
          • 118
          • 119
          • 120
          • 121
          • 122
          • 123
          • 124
          • 125
          • 126
          • 127
          • 128
          • 129
          • 130
          • 131
          • 132
          • 133
          • 134
          • 135
          • 136
          • 137
          • 138
          • 139
          • 140
          • 141
          • 142
          • 143
          • 144
          • 145
          • 146
          • 147
          • 148
          • 149
          • 150
          • 151
          • 152
          • 153
          • 154
          • 155
          • 156
          • 157
          • 158
          • 159
          • 160
          • 161
          • 162
          • 163
          • 164
          • 165
          • 166
          • 167
          • 168
          • 169
          • 170
          • 171
          • 172
          • 173
          • 174
          • 175
          • 176
          • 177
          • 178
          • 179
          • 180
          • 181
          • 182
          • 183
          • 184
          • 185
          • 186
          • 187
          • 188
          • 189
          • 190
          • 191
          • 192
          • 193
          • 194
          • 195
          • 196
          • 197
          • 198
          • 199
          • 200
          • 201
          • 202
          • 203
          • 204
          • 205
          • 206
          • 207
          • 208
          • 209
          • 210
          • 211
          • 212
          • 213
          • 214
          • 215
          • 216
          • 217
          • 218
          • 219
          • 220
          • 221

          客户端

          #include <stdio.h>
          #include <stdlib.h>
          #include <string.h>
          #include <errno.h>
          #include <unistd.h>
          
          #include <arpa/inet.h>
          #include <sys/socket.h>
          
          
          #define ERR_MSG(err_code) do {                                     \
              err_code = errno;                                              \
              fprintf(stderr, "ERROR code: %d \n", err_code);                \
              perror("PERROR message");                                      \
          } while (0)
          
          const int BUF_LEN = 100;
          
          
          int main(void)
          {
              /* 配置 Server Sock 信息。*/
              struct sockaddr_in srv_sock_addr;
              memset(&srv_sock_addr, 0, sizeof(srv_sock_addr));
              srv_sock_addr.sin_family = AF_INET;
              srv_sock_addr.sin_addr.s_addr = inet_addr("192.168.1.3");
              srv_sock_addr.sin_port = htons(8086);
          
              int cli_socket_fd = 0;
              char send_buff[BUF_LEN];
              char recv_buff[BUF_LEN];
          
              /* 永循环从终端接收输入,并发送到 Server。*/
              while (1) {
          
                  /* 创建 Client Socket。*/
                  if (-1 == (cli_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
                  {
                      printf("Create socket ERROR.\n");
                      ERR_MSG(errno);
                      exit(EXIT_FAILURE);
                  }
          
                  /* 连接到 Server Sock 信息指定的 Server。*/
                  if (-1 == connect(cli_socket_fd,
                                    (struct sockaddr *)&srv_sock_addr,
                                    sizeof(srv_sock_addr)))
                  {
                      printf("Connect to server ERROR.\n");
                      ERR_MSG(errno);
                      exit(EXIT_FAILURE);
                  }
          
                  /* 从 stdin 接收输入,再发送到建立连接的 Server Socket。*/
                  fputs("Send to server> ", stdout);
                  fgets(send_buff, BUF_LEN, stdin);
                  send(cli_socket_fd, send_buff, BUF_LEN, 0);
                  memset(send_buff, 0, BUF_LEN);
          
                  /* 从建立连接的 Server 接收数据。*/
                  recv(cli_socket_fd, recv_buff, BUF_LEN, 0);
                  printf("Recevice from server: %s\n", recv_buff);
                  memset(recv_buff, 0, BUF_LEN);
          
                  /* 每次 Client 请求和响应完成后,关闭连接。*/
                  close(cli_socket_fd);
              }
          
              return EXIT_SUCCESS;
          }
          
            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
          • 68
          • 69
          • 70

          测试

          $ gcc -g -std=c99 -Wall tcp_server.c -o tcp_server
          $ gcc -g -std=c99 -Wall tcp_client.c -o tcp_client
          
          $ ./tcp_server
          $ ./tcp_client
          
            2
          • 3
          • 4
          • 5

          相关技术文章

          点击QQ咨询
          开通会员
          返回顶部
          ×
          微信扫码支付
          微信扫码支付
          确定支付下载
          请使用微信描二维码支付
          ×

          提示信息

          ×

          选择支付方式

          • 微信支付
          • 支付宝付款
          确定支付下载