博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
网络协议---TCP---socket bind listen accept listen函数 未决连接队列
阅读量:3920 次
发布时间:2019-05-23

本文共 8795 字,大约阅读时间需要 29 分钟。

扩展:

???SO_SNDBUF???

在这里插入图片描述
1、传输层协议主要用于主机的进程与进程之间的相互通信
2、网络层协议主要应用于主机与主机之间的相互通信,
所以网络通信本质上是进程间通信。

套接字socket1、是一种文件,用于进程间网络通信的文件类型→可使用文件描述符引用套接字		与管道类似的,区别在于			1、管道主要应用于本地进程间通信,			2、套接字一般应用于网络进程间(不同主机的进程间)数据的通信,2、本质是内核创建的缓冲区(连接结束后也由内核释放)

在TCP/IP协议中,是用“IP地址(互联网中主机地址)+TCP/UDP端口号(唯一的标识进程)”的方式来进行网络通信

1、socket:创建初始套接字(未绑定IP和port)

在这里插入图片描述

Socket函数中的三个参数其实就是把抽象的socket具体化的条件,
1、domain参数决定了图中所示的第二层通信域
2、type决定了第三层的通信模式
3、protocol决定了第四层真正的通信协议。

#include 
int socket(int domain, int type, int protocol);1、domain: 【网络协议通信】类型  AF_INET:表示使用IPv4通信   AF_INET6 表示使用IPv6通信   AF_UNIX:本地协议,就是当客户端和服务器在同一台电脑的时候使用,(Unix和Linux系统上使用)。2、type: 套接字的类型,常用的有流式套接字/数据报套接字。   SOCK_STREAM(流式套接字) 默认协议:TCP  SOCK_DGRAM(数据报套接字) 默认协议:UDP  SOCK_RAW(原始套接字) ICMP协议。(ping、traceroute使用该协议)3、protocol: 确定通信协议内细分的具体通信方式//0表示使用默认协议 4、返回值说明: 成功返回一个新的文件描述符;失败返回-1,同时设置errno

2、bind::将ip+port和socket创建的套接字绑定在一起

#include 
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数说明: 1、sockfd:socket函数返创建的套接字文件描述符2、addr:【套接字地址结构体指针】(包括了IP地址和端口号) 通用指针类型:实际上是可以接受多种协议的sockaddr结构体, 若=0则内核会随机绑定一个临时端口。 3、addrlen:是参数addr这个套接字地址的结构大小,即sizeof(struct sockaddr) 以适应不同长度的sockaddr 4、返回值:成功返回0,失败返回-1同时设置errno变量绑定的端口号应1024~65535范围 且没有被其他进程占用【1-1023作为熟知保留端口,功能固定,1024-65535自由端口】include
struct sockaddr {
unsigned short sa_family; // 2 bytes address family, AF_xxx 地址族 char sa_data[14]; // 14 bytes of protocol address}; // IPv4 AF_INET sockets:struct sockaddr_in {
short sin_family; // 2 bytes e.g. AF_INET, AF_INET6 unsigned short sin_port; // 2 bytes e.g. htons(3490) struct in_addr sin_addr; // 4 bytes see struct in_addr, below char sin_zero[8]; // 8 bytes zero this if you want to};注释中标明了属性的含义及其字节大小,这两个结构体一样大,都是16个字节,而且都有family属性,不同的是:sockaddr用其余14个字节来表示sa_data,而sockaddr_in把14个字节拆分成sin_port, sin_addr和sin_zero分别表示端口、ip地址。sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小。sockaddr和sockaddr_in包含的数据都是一样的,但他们在使用上有区别: 程序员不应操作sockaddr,sockaddr是给操作系统用的 程序员应使用sockaddr_in来表示地址,sockaddr_in区分了地址和端口,使用更方便。一般的用法为: 程序员把类型、ip地址、端口填充sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数 struct in_addr {
unsigned long s_addr; // 4 bytes load with inet_pton()};

主动socket和被动socket

socket函数创建的socket是主动socket,主动的socket可以调用connect跟一个被动socket建立一个连接被动socket:主动socket经过listen处理后变成被动(监听)socket,当被动socket接受一个连接通常称为被动打开。  服务端会作为被动socket被动接受连接客户端会作为主动socket主动发起连接

三、listen函数

功能:

  1、将主动socket转换成被动socket→套接字从CLOSED状态转换为LISTEN状态。
  2、限制(指定)服务端同一时刻所能接受客户端连接请求的个数。
  3、如果套接字 sockfd 没有显示调用bind函数绑定指定的套接字地址的话,listen函数会选择本地ip地址,并随机选择一个端口号绑定到 sockfd上,但是一般作为服务器程序,通常会显示调用 bind 函数。

#include 
int listen(int sockfd, int backlog); 参数说明: 1、sockfd: 输入的主动socket👉转换成被动socket(服务端套接字文件描述符) 👉套接字从CLOSED状态转换为LISTEN状态。 若没有显示调用binf,则listen会选择本地IP地址 + 随机端口 但是一般作为服务器程序,通常会显示调用 bind 函数 2、backlog: Linux内核2.2之前,backlog大小包括半连接状态和全连接状态两种队列大小、 Linux内核2.2之后,分离为两个backlog来分别限制半连接(SYN_RCVD状态)队列大小和全连接(ESTABLISHED状态)队列大小。   SYN queue 队列长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,默认为2048。   Accept queue 队列长度由 /proc/sys/net/core/somaxconn 和使用listen函数时传入的参数,二者取最小值。默认为128。   在Linux内核2.4.25之前,是写死在代码常量 SOMAXCONN ,   在Linux内核2.4.25之后,在配置文件 /proc/sys/net/core/somaxconn 中直接修改,   或者在 /etc/sysctl.conf 中配置 net.core.somaxconn = 128 。3、返回值:成功返回0,失败返回-1

在这里插入图片描述

未决连接队列

分2部分:1、半连接队列SYN queue		没有完成三次握手//完成三次握手后会被放入Accept queue的队尾2、全连接队列Accept queue	完成三次握手,等待被accept

  但是客户端可能会在服务器调用accept之前调用connect,这种情况是有可能发生的,如果此时服务端可能正忙于处理其他客户端,这将产生一个未决连接。系统内核有一个未决连接队列会记录所有未决连接的信息,这样服务器在后面调用accept时就能够处理这些未决连接了,而backlog参数就是用来限制这种未决连接数量的。

  如果未决连接队列已经满了,当接收到更多的连接请求就会忽略,也就是说客户端调用connect函数可能会阻塞,直到未决连接队列中的未决连接被accept为止(注意:当一个连接被accept时就会从未决连接队列删除,此时未决连队列有空位了)。 ### 当全连接队列已满且半连接队列未满的情况下: 当客户端发起一个syn分节时,服务端不会丢弃该syn分节,而是直接响应ack和syn,这时客户端响应ack,并成为established状态,而服务端收到ack响应后,试图将该syn分节从半连接队列中移除,并加入全链接队列,然后由于全连接队列已经满了,这时,在默认情况下,服务端啥也不做,而且不会将该连接由SYN_RECV变成ESTABLISHED,服务端仅仅只是创建一个定时器,以固定间隔重传syn和ack到服务端,直到到达系统默认的synack重传阖值,然后服务端将处于半连接队列里面的syn分节丢弃,此时服务端只剩2个连接。这个时候客户端一侧的连接仍然有5个established ### 当全连接已满且半连接队列也满的情况下: 当客户端发起一个syn分节时,服务端发现半连接队列已经满了,同时该syn分节尚未重传过,服务端直接丢弃该syn分节,然后客户端过了4秒重传syn分节,这个时候服务端发现半连接队列已满同时,该syn分节已经重传过了,服务端收下了该syn分节,并响应客户端syn+ack,客户端收到syn+ack后,响应ack。客户端三次握手已经完成,而服务端收到ack之后,发现全连接队列是满的,这个时候不会将该连接从SYN_RECV转换成ESTABLISHED,服务端创建定时器,定时重传syn+ack, 直到到达系统默认的synack重传阖值,然后服务端将处于半连接队列里面的syn分节丢弃 [具体实例如下](https://www.cnblogs.com/menghuanbiao/p/5212131.html) ### 查询:ss ```c [root@localhost ~]# ss -l State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 *:http *:* LISTEN 0 128 :::ssh :::* LISTEN 0 128 *:ssh *:* LISTEN 0 100 ::1:smtp :::* LISTEN 0 100 127.0.0.1:smtp *:* ``` 在LISTEN状态,其中 Send-Q 即为Accept queue的最大值,Recv-Q 则表示Accept queue中等待被服务器accept()。

另外客户端connect()返回不代表TCP连接建立成功,有可能此时accept queue 已满,系统会直接丢弃后续ACK请求;客户端误以为连接已建立,开始调用等待至超时;服务器则等待ACK超时,会重传SYN+ACK 给客户端,重传次数受限 net.ipv4.tcp_synack_retries ,默认为5,表示重发5次,每次等待30~40秒,即半连接默认时间大约为180秒,该参数可以在tcp被洪水攻击是临时启用这个参数。

查看SYN queue 溢出

[root@localhost ~]# netstat -s | grep LISTEN102324 SYNs to LISTEN sockets dropped

查看Accept queue 溢出

[root@localhost ~]# netstat -s | grep TCPBacklogDropTCPBacklogDrop: 2334

四、accept函数

当客户端和服务端建立tcp连接时【完成三次握手之后】,服务器可以调用accept函数从未决连接队列中的半连接队列的队头取出一个连接,同时由内核自动创建一个全新的socket 文件描述符并返回。【并分配资源】

  通信的关键在于服务器进程通过内核自动创建的新socket和客户端的socket进行网络通信,换句话说,服务端和客户端进行通信使用的其实是accept函数返回的套接字。

调用accpet函数阻塞等待取出【已完成队列队尾的】客户端连接

  1、如果【已完成队列 】为空,即【没有客户端和服务器建立连接】→accpet则阻塞等待
  2、当客户端调用connect函数发起连接→请求进入【未完成队列】→内核完成tcp三次握手(服务器收到客户端ACK)→从【未完成队列 SYN_RCVD状态】(服务器端口状态:SYN_RCVD)进入【已完成队列的队尾】(服务器端口状态:ESTABLISHED)→此后accpet函数会从【已完成队列】取出一个客户端连接然后立即返回。
  3、accpet函数调用成功,返回的是一个用于通信的服务器端socket
  4、accept函数生效于TCP连接之后,故和tcp连接没有任何关系,accept函数只关注未决队列中的【已完成队列有没有成员】

#include 
#include
int accept(int sockfd , struct sockaddr *addr , socklen_t *addrlen);参数说明: sockdf: 服务器被动(监听)socket,函数返回的服务器通信socket衍生自该socket addr: 输出参数(保存客户端地址信息IP + port),若不关心,参数addrlen和addr可以传NULL addrlen: 传入传出参数,传入sizeof(addr)大小,函数返回时返回真正接收到套接字地址结构体的大小返回值:  成功返回一个新的用于和客户端通信的socket  失败返回-1,设置errno变量。(真正用于网络通信)

在这里插入图片描述

为了避免DDSO攻击,accept(期间分配资源)调用放到三次握手之后

由于accept()函数是要分配资源的故accept()不能放在接收到SYN请求(第一次握手)之后,是为了避免DDOS攻击比如:若有10000个客户端都和该服务端进行连接,发送SYN,服务端收到之后,这些客户端却不再理会服务端的回复,然而此时服务端的资源却都用accept()分配了。这就是所谓的“DDOS攻击”。三次握手完成后(客户端和服务器建立了tcp连接)。这时可以调用accept函数获得此连接并分配资源

5、网络通信中的read和write函数:

read函数和write函数

  read函数和write函数相信大家已经非常熟悉了,因为我们在linux系统编程文件io时已经学习过了,这里我们主要是了解read函数和write函数在网络通信中出错是如何处理的。

关于read函数的返回值:

  返回值 > 0,read函数返回实际读取到的字节数
  返回值 < 0,出错,同时设置errno变量
  
  如果数据读取完了,还继续read,就会阻塞。在网络通信中,如果通信链路的对端被关闭的话,那么read函数此时会返回0,这和我们之前学习的管道的读写行为特性是类似的,如果一端关闭,read则返回0。实际上,read函数读到了文件尾也会返回0,会收到EOF字符(系统会发送一个EOF字符给read函数)。

关于write函数的返回值:

  返回值 > 0,write函数返回实际写入的字节数
  
  对于 write函数来说,如果对端已经关闭了还继续write的话,根据之前学习管道的行为特性可知,该函数会返回-1并设置errno变量为EPIPE,即会出现管道破裂情况,同时进程收到SIGPIPE信号。

close函数

  close函数执行之后默认动作是把该套接字标记为关闭,并且不再允许进行read/write操作,当有试图对已经关闭的套接字描述符进行read/write操作时都会接收到一个错误。

#include <unistd.h>

int close(int sockfd);

如果有多个文件描述符引用了一个socket的话,那么调用close函数只是会将该套接字的【文件描述符引用计数】减1,而并不会真正关闭tcp连接,只有当所有的文件描述符都关闭之后,tcp连接才会真正终止。

  通常在C/S模型中用于通信的套接字文件描述符引用计数是大于1的(客户端和服务端都会对该套接字),这意味着调用close并不一定会释放tcp连接。

6、connect函数(主动发起TCP连接)

TCP是全双工(同时双向传输)

如果socket类型是 SOCK_STREAM(即tcp通信)的,调用connect 函数会做两件事:

 1 . 发起请求连接。
 2 . connect函数会自动检测是否有绑定套接字(ip地址和端口号),如果没有,会自动绑定一个可用的套接字地址。(类似linsten,如果没有事先bind一个端口号则会自动绑定一个端口号)

#include 
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数说明: sockdf:客户端的套接字文件描述符 addr:要连接的目标套接字地址(IP地址和端口号)addrlen:=sizeof(addr)返回值说明:连接建立成功返回0,失败返回-1并设置errno 【在建立tcp连接成功或失败才返回】

connect函数出错情况

网络编程的难点在于网络出错的异常处理,熟悉网络编程接口的出错和处理对我们进行网络排错也有很大的帮助。

由于connect函数是在建立tcp连接成功或失败才返回,返回成功的情况我们就直接跳过了,这里我们针对connect函数返回失败的几种情况了解下,一般来说,connect函数返回失败,有以下三种情况:https://blog.csdn.net/qq_35733751/article/details/80636224

其他参考https://blog.csdn.net/qq_35733751/article/category/7567772/1

7、未决连接队列(管理TCP连接请求和状态)

未决连接队列:服务器接存储尚未被处理的客户端连接请求(未完成队列 + 已完成队列)

  已完成连接的请求一旦被accpet接受就会从队列中移除

客户端发起connect函数请求:

  0、服务器收到连接请求,然后检查未决连接队列是否有空位
  1、如果未决连接队列有空位,就将该连接加入未决连接队列
  2、如果未决队列满了,就会拒绝连接👉connect返回失败
图3 - 未决连接队列
未决连接队列中又分为2个队列:
1、未完成连接队列(未完成三次握手):即客户端已经发出SYN报文并到达服务器,但是在tcp三次握手连接完成之前,服务器处于SYN_RCVD状态。
2、已完成连接队列(已完成三次握手):即完成tcp三次握手的tcp连接,服务器处于ESTABLISHED状态,服务器会将这些套接字加入到已完成队列。【还没有被accept,一旦被accept就会离开队列】
在这里插入图片描述

对于这两个队列需要注意几点注意:

  1 . (旧linux版本:)未完成队列和已完成队列的总和不超过listen函数设定的backlog参数的大小
  2 . 一旦该连接的tcp三次握手完成,就会从【未完成队列】转到【已完成队列】中
  3 . 如果未决连接队列已满,当又接收到一个客户端SYN时,服务端的tcp将会忽略该SYN,也就是不会理客户端的SYN,但是服务端并不会发送,原因是:客户端tcp可以重传SYN,并期望在计时器超时前从未决连接队列中找到空位与服务端建立连接,这显然是我们所希望看到的。但如果让服务端直接发送一个RST的话,那么客户端的connect函数将会立即返回一个错误,而不会让tcp有机会重传SYN,但是我们并不建议这样做。
  但是不排除有些linux实现在未决连接队列满时,的确会发送RST。但是这种做法是不正确的,因此我们最好忽略这种情况,处理这种额外情况的代码也会降低客户端程序的健壮性。
  
在这里插入图片描述

接收缓冲区窗口rwnd

上面就是客户端和服务端在网络中的状态变迁的具体过程,前面我们在学习tcp三次握手的过程中还知道,服务端和客户端在建立连接的时候会设置自己的一个接收缓冲区窗口rwnd的大小。

服务端在发送SYN + ACK数据报文时会设置并告知对方自己的接收缓冲区窗口大小,客户端在发送ACK数据报文时也会设置并告知对方自己的接收缓冲区窗口大小。

进程终止的部分工作是关闭所有打开的描述符,因此客户打开的描述符由内核关闭。

你可能感兴趣的文章
搞懂Netty(3)使用MessagePack解决编解码问题
查看>>
21、可重入排它锁ReentrantReadWriteLock使用详解
查看>>
1、mysql基本操作上,适合新手和巩固学习
查看>>
2、mysql基本操作中
查看>>
22、一个带有邮戳的锁StampedLock(jdk1.8出现)
查看>>
23、详解java中一个分而治之的框架ForkJoin
查看>>
2、java中的日志框架体系梳理(以故事的形式呈现)
查看>>
为什么我选用了springcloud而不是dubbo
查看>>
3、mysql中的事务操作
查看>>
4、mysql中的视图
查看>>
手牵手一起学Springcloud(1)微服务的理解
查看>>
3分钟学会mysql数据库的逻辑架构原理
查看>>
5、mysql中的变量
查看>>
分布式系统中的CAP理论,面试必问,你理解了嘛?
查看>>
2、BASE理论
查看>>
12、java中的异常体系
查看>>
3、面试官让我手写一个平衡二叉树,我当时就笑了
查看>>
java中的两种排序工具Arrays和Collections的使用
查看>>
1、并查集
查看>>
13、java中==和equals的区别
查看>>