TCP 连接概述

TCP 的多连接处理(Multiplexing)


使用 accept() 等待新连接, 程序的执行会进入 阻塞 状态, 直到新连接变得可用.
使用 recv() 读取数据, 程序的执行也会进入 阻塞 状态, 直到新数据变得可用.
套接字接口默认都会导致阻塞.
对于服务器, 如果服务器只接受一个连接并且只服务一个客户端, 阻塞就不是个问题.
如果服务器要服务多个客户端, 当其与较慢的客户端建立连接, 客户端发送数据前可能要花费相当长的时间.
在此期间, 服务器在等待 recv() 函数返回, 如果此时其他的客户端尝试连接服务器, 就只能等待.
对于客户端, 以 web 浏览器为例, 浏览器要能够并行(parallel, 平行) 下载图片, 脚本以及其他资源.
标签页(tab) 功能也要求浏览器能够并行的加载多个网站的整张网页.
总之, 真正的应用要求服务器和客户端都能同时管理多个连接, 不能只接受一个连接.

几种方式, 同时处理多个独立的连接


  • 轮询 非阻塞套接字(Polling non-block sockets)
  • 有多种方式可以将套接字的操作配置成使用 非阻塞式的.
    非阻塞模式下, 调用 recv(), 如果没有数据, 函数会立即返回.
    用这种方式组织程序, 可以不停的按顺序轮流检查每一个活动的套接字.
    如果套接字返回数据, 就处理; 未返回, 就忽略.
    这种方式称为 轮询.
    如果大多数时间都读不到数据, 轮询就有些浪费系统资源.
    轮询还要求手动追踪套接字是否活动及套接字所处的状态, 增加了程序的复杂性.
    非阻塞式的函数, 其返回值与阻塞式的也不同, 要采用不同的处理方法.

  • 分叉 和 多线程(Forking and Multithreading)
  • 可以为每个连接开启一个新的 线程/进程, 这样, 套接字阻塞也没关系.
    他只是阻塞自己服务的 线程/进程, 没办法阻塞其他的 线程/线程.
    但正确的处理线程有一定的难度, 尤其当连接之间共享状态的时候更是如此.
    也不利于程序在不同系统之间的可移植性, 不同系统创建新进程的方式并不相同.
    清除多 线程/进程 程序的缺陷(Debugging), 相比单 线程/进程 程序也更困难.

  • select() 函数
  • 通过给 select() 函数提供一个套接字集, 函数可以告诉我们其中哪些套接字的数据已经可读, 哪些已经可写, 以及哪些属于异常.
    select() 也可以配置成若一段时间内无事发生就返回.
    本书采用的方式.

通过 select() 进行同步多连接处理


select() 的原型:

  1. int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

调用 select() 之前, 要将用到的套接字添加进套接字集合.
将 socket_listen, socket_a, socket_b 加入集合 sockets_set:

  1. fd_set sockets_set;
  2. FD_ZERO(&sockets_set);
  3. FD_SET(socket_listen, &sockets_set);
  4. FD_SET(socket_a, &sockets_set);
  5. FD_SET(socket_b, &sockets_set);
FD_CLR()从集合中删除套接字
FD_ISSET()检查集合中是否包含某套接字

select() 还需要一个数字参数, 该参数要大于所有套接字的描述符.
先保存最大的描述符:

  1. int max_socket = socket_listen;
  2. if (socket_a > max_socket) max_socket = socket_a;
  3. if (socket_b > max_socket) max_socket = socket_b;

调用 select() 会改变集合 socket_set, 以表明哪些套接字已经可读/ 可写/ 异常.
所以, 先备份 socket_set, 再调用.

  1. fd_set copy = sockets_set;
  2. select(max_socket+1, &copy, 0, 0, NULL);

程序阻塞直到至少有一个套接字变得可读.
当 select() 返回, copy 已经变成只包含那些可读套接字的集合.
检查一下 copy 包含哪几个可读的套接字.

  1. if (FD_ISSET(socket_listen, &copy)) {
  2. accept(socket_listen ...
  3. }
  4. if (FD_ISSET(socket_a, &copy)) {
  5. recv(socket_a ...
  6. }
  7. if (FD_ISSET(scoket_b, &copy)) {
  8. recv(socket_b ...
  9. }

想要检测那些可写的套接字, 只需将 &copy 作为 select() 函数的第三个参数传递.
想要检测那些异常的套接字, 只需将 &copy 作为 select() 函数的第四个参数传递.

select() 的最后一个参数用来设置超时时间.

  1. struct timeval {
  2. long tv_sec; /* 表示秒数 */
  3. long tv_usec; /* 表示微秒数 */
  4. }

如果希望 select() 最多等待 1.5 秒:

  1. struct timeval timeout;
  2. timeout.tv_sect = 1;
  3. timeout.tv_usec = 500000;
  4. select(max_socket+1, &copy, 0, 0, &timeout);

这样, 当 select() 返回时, 要么 copy 中包含一个可读的套接字, 要么 1.5 秒时间已耗尽.
如果 tv_sec, tv_usec 都置为 0 再传入, 那么 select() 会对 copy 做一些设置, 然后立即返回.
如果传入一个空指针(NULL), 只有当至少一个套接字变为可读时, select() 才返回.

一次调用, 三种检查:

  1. select(max_socket+1, &ready_to_read, &ready_to_write, &excepted, &timeout);

关于 select() 返回值, 函数返回的是包含在三种集合(可读, 可写, 异常)内的描述符的总数.
如果返回 0, 意味着超时.
如果函数执行过程中出错, 则返回 -1.

关闭集合 copy 中的所有套接字:

  1. int i;
  2. for (i = 1; i <= max_socket; ++i) {
  3. if (FD_ISSET(i, &copy)) {
  4. close(i);
  5. }
  6. }

这一操作有些暴力, 但除非这一操作成为显著的性能瓶颈, 否则, 并没有优化的必要.

select() 不只用于套接字程序


在类 Unix 的系统, select() 还可以用于文件和终端 I/O, 是非常有用的函数.

《C 语言网络编程实践》(《Hands-On Network Programming with C》)