三、 理解listen的第二个参数
在编写TCP套接字的服务器代码时,在进行了套接字的创建和绑定之后,需要调用listen函数将创建的套接字设置为监听状态,此后服务器就可以调用accept函数获取建立好的连接了。其中listen函数的第一个参数是需要设置为监听状态的套接字,而listen的第二个参数一般设置为5,那你知道listen函数的第二个参数具体的含义是什么吗?
实验
下面通过一个实验来说明listen的第二个参数的具体含义:
先编写TCP套接字的服务器端代码,服务器初始化时依次进行套接字创建、绑定、监听,但服务器初始化后不调用accept函数获取底层建立好的连接。为了方便验证,下面将listen函数的第二个参数设置为2
#include <iostream> #include <cstring> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <netinet/in.h> #include <pthread.h> const int port = 8081; const int num = 2; int main() { //创建监听套接字 int listen_sock = socket(AF_INET, SOCK_STREAM, 0); if (listen_sock < 0){ std::cerr << "socket error" << std::endl; return 1; } int opt = 1; setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //绑定 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_port = htons(port); local.sin_family = AF_INET; local.sin_addr.s_addr = INADDR_ANY; if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){ std::cerr << "bind error" << std::endl; return 2; } //监听 if (listen(listen_sock, num) < 0){ std::cerr << "listen error" << std::endl; return 3; } //启动服务器 for (;;){ //不调用accept获取连接 } return 0; }
启动服务器后使用netstat -nltp命令,可以看到该服务器当前正处于监听状态
接下来使用Postman向服务器发起一个连接请求
注意:为了让实验现象更加明显,最好不要使用浏览器测试,因为浏览器发起连接请求后若得不到响应会进行重发
Postman发起连接请求后,此时通过下面命令就可以发现,此时服务器端新增了一个连接,该连接处于ESTABLISHED状态
sudo netstat -ntp | head -2&&sudo netstat -ntp | grep 8082
继续使用Postman发起第二个、第三个连接请求
此时在服务器端也会新增对应的第二个、第三个连接,现在服务器端已经建立的连接的个数就是3
现在继续使用Postman向服务器发起第四次连接请求
此时没有继续新增状态为ESTABLISHED的连接,而是新增了一个状态为SYN_RCVD的连接
服务器端新增了一个状态SYN_RCVD的连接,即意味着服务器没有对刚才客户端发来的连接请求进行响应
对于刚才状态为SYN_RCVD的连接,由于服务器长时间不对其进行应答,三次握手失败后该连接会被自动释放
实验现象:
无论有多少客户端向服务器发起连接请求,最终在服务器端最多只有三个连接会建立成功
当发来后续的连接请求时,服务器只是收到了该客户端发来的SYN请求,但并没有对其进行响应
listen的第二个参数
TCP在进行连接管理时会用到两个连接队列:
全连接队列(accept队列)。全连接队列用于保存处于ESTABLISHED状态,但没有被上层调用accept取走的连接
半连接队列。半连接队列用于保存处于SYN_SENT和SYN_RCVD状态的连接,即未完成三次握手的连接
全连接队列长度受到listen第二个参数影响,一般TCP全连接队列的长度等于listen第二个参数的值加一。因为实验时设置listen第二个参数的值为2,所以服务器端全连接队列长度就为3,服务器最多只允许有三个处于ESTABLISHED状态的连接
若将实验代码中listen的第二个参数值设置为3,服务器就最多允许存在4个处于ESTABLISHED状态的连接。在服务器端已有4个ESTABLISHED状态的连接的情况下,再有客户端发来建立连接请求,服务器端就会新增状态为SYN_RCVD的连接,该连接暂时存放在半连接队列中
若半连接队列中也存放满了,此后就算再有客户端发来连接请求,在服务器端也不会新增任何状态的连接
为什么底层要维护连接队列?
一般当服务器压力较大时连接队列的作用才会体现出来,若服务器压力本身就不大,那么一旦底层有连接建立成功,上层就会立马将该连接读走并进行处理
服务器端启动时一般会预先创建多个服务线程为客户端提供服务,主线程从底层accept上来连接后就可以将其交给这些服务线程进行处理。若向服务器发起连接请求的客户端很少,那么连接一旦在底层建立好就被主线程立马accept上来并交给服务线程处理了。但若向服务器发起连接请求的客户端非常多,当每个服务线程都在为某个连接提供服务时,底层再建立好连接主线程就不能获取上来了,此时底层这些已经建立好的连接就会被放到连接队列中,只有等某个服务线程空闲时,主线程就会从这个连接队列中获取建立好的连接。
若没有连接队列,那么当服务器端的服务线程都在提供服务时,其他客户端发来的连接请求就会直接被拒绝。但有可能正当这个连接请求被拒绝时,某个服务线程提供服务完毕,此时这个服务线程就无法立马得到一个连接为之提供服务,所以一定有一段时间内这个服务线程是处于闲置状态的,直到再有客户端发来连接请求。
而若设置了连接队列,当某个服务线程提供完服务后,若连接队列中有建立好的连接,那么主线程就可以立马从连接队列中获取一个连接交给该服务线程进行处理,此时就可以保证服务器几乎是满载工作的,从而提高效率
为什么连接队列不能太长?
全连接队列不能太长,系统一般设置为5。虽然维护连接队列能让服务器处于几乎满载工作的状态,但连接队列也不能设置得太长
若队列太长,也意味着在队列较尾部的连接需要等待较长时间才能得到服务,此时客户端的请求也就迟迟得不到响应
服务器维护连接也是需要成本的,连接队列设置的越长,系统就要花费越多的成本去维护这个队列。与其与其维护一个长连接,造成客户端等待过久,并且占用大量暂时用不到的资源,还不如将部分资源节省出来给服务器使用,让服务器更快的为客户端提供服务
全连接队列的长度
全连接队列的长度由两个值决定:
用户层调用listen时传入的第二个参数backlog
系统变量net.core.somaxconn,默认值为128
通过以下命令可以查看系统变量net.core.somaxconn的值
sudo sysctl -a | grep net.core.somaxconn
全连接队列长度等于listen传入的backlog和系统变量net.core.somaxconn中的较小值加一
四、SYN洪水
连接正常建立的过程
当客户端向服务器发起连接建立请求后,服务器会对其进行SYN+ACK响应,并将该连接放到半连接队列(syns queue)中
当服务器发出的SYN+ACK得到客户端ACK响应后,就会将该连接由半连接队列移到全连接队列(accept queue)中
此时上层就可以通过调用accept函数,从全连接队列中获取建立好的连接了
连接建立异常
若客户端在发起连接建立请求后突然死机或掉线,那么服务器发出的SYN+ACK就得不到对应的ACK应答
这种情况下服务器会进行重试(再次发送SYN+ACK给客户端)并等待一段时间,最终服务器会因为收不到ACK应答而将这个连接丢弃,这段时间长度就称为SYN timeout
在SYN timeout时间内,这个连接会一直维护在半连接队列中
服务器虽然需要短暂维护这些异常连接,但这种情况毕竟是少数,不会对服务器造成太大影响
SYN洪水
但若有一个恶意用户大量模拟这种情况:向服务器发送大量的连接建立请求,但收到服务器的SYN+ACK后故意不对其进行ACK应答
此时服务器就需维护一个非常大的半连接队列,并且这些连接最终都不会建立成功,也就不会被移到全连接队列中供上层获取,最后会导致半连接队列越来越长。
当半连接队列被占满后,新来的连接就会直接被拒绝,哪怕是正常的连接建立请求,此时就会导致正常用户无法访问服务器
这种向服务器发送大量SYN请求,但并不对服务器的SYN+ACK进行ACK响应,最终可能导致服务器无法对外提供服务的行为,就被称为SYN洪水攻击(SYN Flood)
如何解决SYN洪水攻击?
TCP作为传输控制协议需要对其进行处理,而上层应用层也要尽量避免遭到SYN洪水攻击。
应用层可以记录,向服务器发起连接建立请求的主机信息,若发现某个主机多次向服务器发起SYN建立连接请求,但从不对服务器的SYN+ACK进行ACK响应,此时就可以对该主机进行黑名单认证,此后该主机发来的SYN请求一概不进行处理
TCP为了防范SYN洪水攻击,保证正常用户使用,引入了syncookie机制:
核心问题就在于半连接队列被占满了,但不能简单的扩大半连接队列,就算半连接队列再大,恶意用户也会发送更多的SYN请求占满,并且维护半连接队列中的连接也是需要成本的
因此TCP引入了syncookie机制,当服务器收到一个连接建立请求后,会根据这个SYN包计算出一个cookie值,将其作为将要返回的SYN+ACK包的初始序号,然后将这个连接放到一个暂存队列中。当服务器收到客户端的ACK响应时,会提取出当中的cookie值进行对比,对比成功则说明是一个正常连接,此时该连接就会从暂存队列中移到全连接队列供上层读取,保证正常用户的使用
引入了syncookie机制的好处:
引入syncookie机制后,异常连接就不会堆积在半连接队列中了,也就不会出现半连接队列被占满的情况了
对于正常的连接,一般会立即对服务器的SYN+ACK进行ACK应答,因此正常连接会很快建立成功
异常的连接,不会对服务器的SYN+ACK进行ACK应答,因此异常的连接最终都会堆积到暂存队列中,最终被处理
五、TCP、UDP对比
TCP协议
TCP协议被称为传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议
TCP协议是面向连接的,若两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法
UDP协议
UDP协议被称为用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议
使用UDP协议进行通信时无需建立连接,若两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。
TCP/UDP对比
TCP协议虽然是保证可靠性的协议,但不能说TCP就一定比UDP好,因为TCP保证可靠性也就意味着TCP需要做更多的工作,而UDP不保证可靠性也就意味着UDP足够简单
TCP常用于可靠传输的情况,应用于文件传输,重要状态更新等场景
UDP常用于对高速传输和实时性较高的通信领域,例如早期的QQ、视频传输等。另外UDP可以用于广播
UDP和TCP没有谁最好,只有谁最合适,网络通信时具体采用TCP还是UDP完全取决于上层的应用场景
六、优化UDP实现可靠传输(面试题)
当面试官让你用UDP实现可靠传输时,一定要立马想到TCP协议,因为TCP协议就是当前比较完善的保证可靠性的协议,面试官要求用UDP这个不可靠的协议来实现可靠传输,无非就是要求在应用层来实现可靠性,此时就可以参考TCP协议保证可靠性的各种机制
例如:
引入序列号,保证数据按序到达
引入确认应答,确保对端接收到了数据
引入超时重传,若隔一段时间没有应答,就进行数据重发
…
当被面试官问到这个问题时,最好与面试官进一步进行沟通,比如询问这个用UDP实现可靠传输的应用场景是什么。因为TCP保证可靠性的机制太多了,但在某些场景下可能只需要引入TCP的部分机制即可,因此在不同的应用场景下模拟实现UDP可靠传输的侧重点也是不同的