BruceChen7 / gitblog

My blog
6 stars 1 forks source link

网络编程 #20

Open BruceChen7 opened 4 years ago

BruceChen7 commented 4 years ago

学习路线

Ethernet frame

指标

客户端如何关闭tcp连接

printf("shutdown write and read until EOF\n");
while ((nr = stream -> receiveSome(buf, sizeof(buf))) > 0) {
}
printf("all down");
// 这里关闭close
TcpStream destructs here, close the tcp socket

客户端和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 仅仅是改变了通用绑定的方式 。在上表中,我们发现如下情况:

SO_REUSEPORT

运行在Linux系统上网络应用程序,为了利用多核的优势,一般使用以下比较典型的多进程/多线程服务器模型:

模型见如评论,使用SO_REUSEPORT模型见评论

怎么解决这个问题呢?在Linux3.9 中,引入了SO_REUSEPORT,其支持多个线程或者进程绑定同一个端口,提高了服务器性能。它解决的问题:

你可以显示的同时设置SO_REUSEADDR和SO_REUSEPORT

SO_REUSEPORT测试

使用python脚本快速构建一个小的示范原型,两个进程,都监听同一个端口10000,客户端请求返回不同内容。server_v1.py,简单PING-PONG:

# -*- coding:UTF-8 -*-

import socket
import os
PORT = 10000
BUFSIZE = 1024

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
s.bind(('', PORT))
s.listen(1)
while True:
    conn, addr = s.accept()
    data = conn.recv(PORT)
    conn.send('Connected to server[%s] from client[%s]\n' % (os.getpid(), addr))
    conn.close()

s.close()

server_v2.py,输出当前时间:


# -*- coding:UTF-8 -*-

import socket
import time
import os

PORT = 10000
BUFSIZE = 1024

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 设置SO_RESUEPORT选项
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
s.bind(('', PORT))
s.listen(1)

while True:
    conn, addr = s.accept()
    data = conn.recv(PORT)
    conn.send('server[%s] time %s\n' % (os.getpid(), time.ctime()))
    conn.close()

s.close()

借助于bindp运行两个版本的程序:


python server_v1.py &
python server_v2.py &

模拟客户端请求10次:


for i in {1..10};do echo "hello" | nc 127.0.0.1 10000;done

看看结果吧:


Connected to server[3139] from client[('127.0.0.1', 48858)]

server[3140] time Thu Feb 12 16:39:12 2015
server[3140] time Thu Feb 12 16:39:12 2015
server[3140] time Thu Feb 12 16:39:12 2015
Connected to server[3139] from client[('127.0.0.1', 48862)]
server[3140] time Thu Feb 12 16:39:12 2015
Connected to server[3139] from client[('127.0.0.1', 48864)]
server[3140] time Thu Feb 12 16:39:12 2015
Connected to server[3139] from client[('127.0.0.1', 48866)]
Connected to server[3139] from client[('127.0.0.1', 48867)]

可以看出来,CPU分配很均衡,各自分配50%的请求量。如果两个程序,其中一个没有设置SO_REUSEPORT,那么直接报错,提示address already in use

BruceChen7 commented 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;
}

处理interrupt的信号

restart:
int n = recv(fd, buf, len, 0);
if (n == -1) {  
    if (errno == EINTR) {
        goto restart;
    }
    perror("recv");
    return -1;
}

异步dns

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的系统调用。