Open BruceChen7 opened 4 years ago
// 几种处理sig_pipe的方式
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
perror("signal");
return -1;
}
// 在调用send api时候指定
int n = send(fd, msg, len, MSG_NOSIGNAL);
// 设置socket选项
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)) == -1) {
perror("setsockopt");
return -1;
}
restart:
int n = recv(fd, buf, len, 0);
if (n == -1) {
if (errno == EINTR) {
goto restart;
}
perror("recv");
return -1;
}
POSIX 提供了 getaddrinfo阻塞的方式,来将域名转换成ip,这对于non-blocking模式的socket是不可以用的,glibc提供了getaddrinfo_a函数来执行异步dns查询,但是和epoll结合的不是很好
// res由getaddrinfo返回
// SOCK_NONBLOCK设置了非阻塞模式
// SOCK_CLOEXEC防止子进程继承文件描述符
int fd = socket(res->ai_family, res->ai_socktype | SOCK_NONBLOCK | SOCK_CLOEXEC, res->ai_protocol);
if (fd == -1) {
perror("socket");
return -1;
}
restart:
int rc = connect(fd, res->ai_addr, res->ai_addrlen);
if (rc == -1 && errno != EINPROGRESS) {
if (errno == EINTR) {
goto restart;
}
perror("connect");
return -1;
}
if (rc == 0) {
// Connection succeeded immediately
} else {
// Connection attempt is in progress
}
当epoll返回该connect fd的状态为EPOLLOUT或者发生错误时EPOLLERR,我们需要再一次检查下该fd
int opt;
socklen_t optlen = sizeof(opt);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &opt, &optlen) == -1) {
perror("getsockopt");
return -1;
}
if (opt != 0) {
// Connection failed
errno = opt;
perror("connect");
return -1;
}
for (;;) {
int fd = accept4(lfd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (fd == -1) {
if (errno == EAGAIN || EWOULDBLOCK) {
break;
}
if (errno == EINTR || errno == ECONNABORTED) {
continue;
}
perror("accept4");
return -1;
}
// Handle new connection
}
注意使用accept4直接将accept fd设置成non-blocking,这样减少一次fcntl的系统调用。
学习路线
layered networking
Ethernet frame
指标
客户端如何关闭tcp连接
客户端和server端该如何正确关闭连接
见评论
SO_REUSEADDR 和 SO_REUSEPORT
资料来源:
总体来说,SO_REUSEADDR有两个功能:
一般来说,一个端口释放后,2分钟才能被利用,SO_REUSEADDR让端口释放后再次被使用。SO_REUSEADDR用于对TCP套接字处于TIME_WAIT状态下的socket,才可以重复绑定使用。server程序总是应该在调用bind()之前设置SO_REUSEADDR套接字选项。注意TCP中,先调用close()的一方会进入TIME_WAIT状态。
关于TIME_WAIT状态,一方面是确保TCP send buffer中的数据能够完全的发送到对方。另一方面是避免数据串号,处于TIME_WAIT状态的发送方,可能等到buffer中的数据都被传输,或者是指定的超时时间到了,那么这时,才是真正关闭套接字。
TIME_WAIT超时时间称之为Linger Time,我们可以通过SO_LINGER来指定时间,甚至是关闭等待时间,显示的关闭等待时间是不合理的,因为不仅是buffer中的数据要在这段时间中发送完成,就是close的四次挥手的数据包也要在这段时间内发送完毕,显然,这是不合理的。
但实现中,尽管你显示的关闭Linger Time,如果你的进程被意外杀死,或者是自己主动杀死,但是没有显示的关闭到套接字。操作系统也会等待相应的时间,忽略你的配置。比如:你的进程调用exit()等。 也就是说TIME_WAIT是必不可少的。
在TIME_WAIT中的连接,没有指定SO_REUSEADDR是什么样的情况呢?套接字中的源端口和源IP地址都不可用,直到Linger Time的时间到了,所以,即使我们调用了close,然后立即rebind,这时绑定是不成功的。
如果你指定了SO_REUSEADDR,如果即使有相同的IP地址和端口号处于TIME_WAIT状态,那么你的绑定也是成功的。但是这可能带来问问题,如果处于TIME_WAIT状态的套接字仍然工作。
关于第一点:
见评论
一个套接字由相关五元组构成,协议、本地地址、本地端口、远程地址、远程端口。SO_REUSEADDR 仅仅是改变了通用绑定的方式 。在上表中,我们发现如下情况:
两个套接字版绑定了同样的IP地址和端口号,一定会出错。(不管是通用IP地址(0.0.0.0),还是具体的IP地址(192.168.1.0),是否开启SO_REUSEADDR选项)
使用了SOCK_REUSEPORT,则对绑定通用IP地址(本地IP地址)(socketA)和具体的IP地址(socketB)起作用。
两个套接字绑定的相同的端口,但是不同的IP地址总是成功的,不管是否使用SOCK_REUSEADDR。
SO_REUSEPORT
运行在Linux系统上网络应用程序,为了利用多核的优势,一般使用以下比较典型的多进程/多线程服务器模型:
模型见如评论,使用SO_REUSEPORT模型见评论
怎么解决这个问题呢?在Linux3.9 中,引入了SO_REUSEPORT,其支持多个线程或者进程绑定同一个端口,提高了服务器性能。它解决的问题:
你可以显示的同时设置SO_REUSEADDR和SO_REUSEPORT
SO_REUSEPORT测试
使用python脚本快速构建一个小的示范原型,两个进程,都监听同一个端口10000,客户端请求返回不同内容。server_v1.py,简单PING-PONG:
server_v2.py,输出当前时间:
借助于bindp运行两个版本的程序:
模拟客户端请求10次:
看看结果吧:
可以看出来,CPU分配很均衡,各自分配50%的请求量。如果两个程序,其中一个没有设置SO_REUSEPORT,那么直接报错,提示address already in use