系统的补充网络编程基础。
基本来源自:《UNIX网络编程》6.3 select函数
chapter04_基本TCP套接字编程中使用的是最基本的阻塞式I/O模型。默认情形下, 所有套接字都是阻塞的。如下图所示:
有了I/O复用(I/O multiplexing), 我们就可以调用select或poll, 阻塞在这两个系统调用中的某一个之上, 而不是阻塞在真正的I/O系统调用上。 (当有套接字需要读取的时候,才调用read,而不是read等待对方发送数据)。如下图所示:
chapter05_TCP客户_服务器示例 中,当客户端与服务器建立连接后,服务端会创建一个子线程来来处理与客户端之后的事情。
I/O复用并不显得有什么优势,事实上由于使用select需要两个而不是单个系统调用,I/O复用还稍有劣势。不过,使用select的优势在于我们可以等待多个描述符就绪。
select允许进程指示内核等待多个事件中的任何一个发生, 并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
它的接口如下所示:
#include <sys/select.h>
#include <sys/time.h>
int select(int nfds, fd_set *readset, fd_set *writeset, fd_set *exceptset,
const struct timeval *timeout);
返回: 若有就绪描述符则为其数目,若超时则为0,若出错则为-1。
我们从该函数的最后一个参数timeout开始介绍,它告知内核等待所指 定描述符中的任何一个就绪可花多长时间。 其timeval结构用于指定这段时间的秒数和微秒数。
这个参数有以下三种可能。
前两种情形的等待通常会被进程在等待期间捕获的信号中断, 并从信号处理函数返回。(小心内核不重启select系统调用。可以参考chapter05_TCP客户_服务器示例)
中间的三个参数readset、 writeset和exceptset指定我们要让内核测试读、 写和异常条件的描述符。
如何给这3个参数中的每一个参数指定一个或多个描述符值是一个设计上的问题。 select使用描述符集, 通常是一个整数数组, 其中每个整数中的每一位对应一个描述符。 举例来说,假设使用32位整数, 那么该数组的第一个元素对应于描述符031,第二个元素对应于描述符3263, 依此类 推。 所有这些实现细节都与应用程序无关, 它们隐藏在名为fd_set的数据类 型和以下四个宏中:
void FD_ZERO(fd_set *fdset); /* clear all bits in fdset */
void FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fdset */
void FD_CLR(int fd, fd_set *fdset); /* trun off the bit for fd in fdset */
int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset */
我们分配一个fd_set数据类型的描述符集, 并用这些宏设置或测试该集合中的每一位, 也可以用C语言中的赋值语句把它赋值成另外一个描述符集。(这个赋值,应该是浅拷贝,应为C中赋值数组,应该只是赋值指针,毕竟没有运算符重载)
nfds参数指定待测试的描述符个数, 它的值是待测试的最大描述符加1。 描述符0, 1, 2, …, 一直到nfds-1均将被测试。(因为描述符是顺序增大分配。测试的不仅仅是参数中的描述符集中的设置的描述符。得遍历fd_set数组,才知道哪些描述符被设置,可读或者可写)
满足下列四个条件中的任何一个时, 一个套接字准备好读。
下列四个条件中的任何一个满足时, 一个套接字准备好写。
参考:《unix网络编程》6.8 TCP回射服务器程序; Linux编程之select
代码也可见仓库:UNP/chapter06
这个代码是有问题的:FD_ISSET将描述符集中的对应位清零,没有重新置1。需要维护一个数组来记录有哪些套接字。
select函数返回后, 我们使用FD_ISSET宏来测试fd_set数据类型中的描述符。 描述符集内任何与未就绪描述符对应的位返回时均清成0。 为此, 每次重新调用select函数时, 我们都得再次把所有描述符集内所关心的位均置为1。
使用select时最常见的两个编程错误是: 忘了对最大描述符加1; 忘了描述符集是值-结果参数。 第二个错误导致调用select时, 描述符集内我们认为是1的位却被置为0。
#include "log.hpp"
#include <sys/socket.h>
#include <bits/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <iostream>
#include <string>
#define LISTENQ 1024
#define BUFF_SIZE 1024
using namespace std;
int main(void)
{
BOOST_LOG_TRIVIAL(info) << "start server.";
string server_port = "10000";
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd < 0) {
BOOST_LOG_TRIVIAL(error) << "Failed to create a listening socket.";
return -1;
}
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(stoi(server_port));
bind(listenfd, (sockaddr *)&servaddr, sizeof(servaddr));
if(listen(listenfd, LISTENQ) < 0) {
BOOST_LOG_TRIVIAL(error) << "Failed to set up the listening socket.";
return -1;
}
fd_set readfds;
FD_ZERO(&readfds);
int nfds = -1; // 记录最大的套接字+1
while(1) {
FD_SET(listenfd, &readfds); // 将监听套接字加入读描述符集
// 永远等待下去,直到存在套接字可读
nfds = listenfd > nfds-1 ? listenfd+1 : nfds;
int n = select(nfds, &readfds, nullptr, nullptr, nullptr);
if(n < 0) {
BOOST_LOG_TRIVIAL(error) << "Select error...";
return -1;
}
for(int fd=0; fd<nfds; fd++) {
if(FD_ISSET(fd, &readfds)) {
if(fd == listenfd) { // 监听套接字已完成的连接数不为0 --> 监听套接字准备好读
int client_sockfd = accept(listenfd, nullptr, nullptr);
FD_SET(client_sockfd, &readfds); // 将客户端套接字加入读描述符集
nfds = client_sockfd > nfds-1 ? client_sockfd+1 : nfds;
} else {
char buffer[BUFF_SIZE] = {0};
int readbytes = read(fd, buffer, BUFF_SIZE);
if(readbytes > 0) {
cout << "receive data: " << buffer;
} else {
close(fd); // 这里需要注意,对于Tcp,能否根据返回read的返回值为零,来作为关闭socket的依据?
FD_CLR(fd, &readfds);
BOOST_LOG_TRIVIAL(info) << "Close socket fd: " << fd;
}
}
}
}
}
close(listenfd);
}
使用nc
作为客户端,连接上面程序,进行测试。