在windows系统中,使用winsock.h 文件 ,他是个接口,而不是协议,应用在tcp/ip协议中。只跟windows操作系统有关,与开发工具无关。
在linux系统中,使用的是sys/socket.h头文件
iso 7层网络参考模型 :
应用层(7)
表示层
会话层
传输层
网络层
数据链路层
物理层(1)
为什么连接建立需要三次握手,而不是两次握手?
防止失效的连接请求报文段被服务端接收,从而产生错误.主要目的防止server端一直等待,浪费资源.
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭.
因为当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能还需要发送一些数据给对方,再发送FIN报文给对方来表示你同意现在可以关闭连接了,故这里的ACK报文和FIN报文多数情况下都是分开发送的,也就造成了4次挥手。
2MSL的作用:
等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。一般MSL设置为30s、1min等
一行32bit为4字节。所以除去选项部分,ip头大小为20字节。
4位版本号
指定IP协议的版本。对IPv4来说,为4。
4位头部长度
标识该IP头部有多少个32bit(即多少行),四位最大表示15,所以IP头部最长为60字节。
8位服务类型
包括一个三位的优先权字段(现在已忽略)四位的TOS字段和一位保留字段(保留需置0).四位的TOS字段分别表示:最小延时,
最大吞吐量,最高可靠性和最小费用。这四位中最多只有一位置1。应用程序根据需要设置(比如ssh需要最小延时,ftp需要
最大吞吐量)。
16位总长度
是指整个IP数据报的长度,以字节为单位,因此IP数据报最大长度为65535-1字节。但由于MTU的限制,长度超过MTU的
数据报都将被分片传输,所以实际传输的IP数据报的长度都远远没有达到最大值。
16位标识:
唯一地标识主机发送的每一个数据报。其初始值由系统随机生成,每发送一个数据报,其值就加一。该值在数据报分片时被
复制到每个分片中,因此同一个数据报的所有分片都具有相同的标识符。
3位标志字段:
第一位保留。第二位标识禁止分片。设置后,IP将不对该数据报分片,当该数据报长度超过MTU时,IP模块将丢弃该数据包
并返回一个ICMP差错报文。第三位表示更多分片。除了数据报的最后一个分片外,其他分片都要把该位置1.
13位分片偏移
是分片相对原始IP数据报开始处(仅指数据部分)的偏移。实际的偏移值是该值左移三位(乘8)后得到的。因此,每个IP分片的
数据部分的长度必须是8的整数倍。
8位生存时间TTL(time to live):
是数据报到达目的地之前允许经过的路由器跳数。发送端设置(通常是64)。数据报每经过一次路由,该值就被路由器减一,
为零时,路由器将丢弃该数据报,并向源端发送一个ICMP差错报文。TTL可以防止数据报陷入路由循环。
8位协议:
用来区分上层协议。/etc/protocols文件定义了所有上层协议对应的字段的数值。例如,ICMP是1,TCP是6,UDP是17。
16位头部校验和:
由发送端填充,接收端对其使用CRC算法检验头部在数据传输过程中是否损坏。
32位源端IP地址:
用来表示IP数据报的发送端
32位目的端IP地址:
用来表示IP数据报的接收端
选项字段:
是可变长的可选信息。最多包含40字节,可用的选项有:
记录路由:将途径的路由器的ip地址填入选项部分,用于跟踪数据报的传递路径。
时间戳:告诉每个路由器将数据报被转发的时间(或时间与IP地址对)填入IP头部的选项部分,这样就可以测量途径路由
之间数据报传输的时间。
松散路由选择:指定路由IP地址列表,数据包发送过程必须经过其中所有路由器。
严格路由选择:数据报只能经过被指定的路由器。
流程
WSAStartup函数用于加载 winsock dll 版本。一般使用 2.2 就可以了。
WSACleanup函数用于释放资源。
SOCKADDR_IN 结构指定ip和端口。
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr; //对服务器而言 对于IPv4来说,通配地址通常由INADDR_ANY来指定, 其值一般为0。它告知内核去选择IP地址
char sin_zero[8];
};
网络字节顺序:即 从最有意义的字节到最无意义的字节。
主机字节顺序:从从最无意义的字节到最有意义的字节。
大端模式:数据高字节存放在内存低地址,数据低字节存放在内存高地址。即地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。
小端模式:高字节存放在高地址,低字节存放在低地址。
Intel的80x86系列芯片是唯一还在坚持使用小端的芯片,ARM芯片默认采用小端,但可以切换为大端;而MIPS等芯片要么采用全部大端的方式储存,要么提供选项支持大端——可以在大小端之间切换。另外,对于大小端的处理也和编译器的实现有关,在C语言中,默认是小端(但在一些对于单片机的实现中却是基于大端,比如Keil 51C),Java是平台无关的,默认是大端。
在网络上传输数据普遍采用的都是大端。
为了跨平台通信,还专门出了网络字节序和主机字节序之间的转换接口(ntohs、htons、ntohl、htonl)
sin_port:使用网络字节顺序,htons、htonl 函数主机字节序转换网络字节序
反之就是 ntohs、ntohl .一般端口使用16位的,即htons。
s_addr:按照网络字节顺序存储IP地址,是一个4字节的ip地址,要考虑和点分法之间的转换,inet_addr 是字符转4字节网络字节顺序。反过来就是inet_ntoa。
在linux中,inet_aton()也是从字符串转换成4字节网络字节序。
ipv4和ipv6都使用的函数
inet_pton()和inet_ntop() p:表示表达,即字符串 n:表示数值
套接字是传输提供程序的句柄, 核心是句柄。
SOCKET PASCAL FAR socket (
__in int af, //AF_INET
__in int type, // SOCK_STREAM 或 SOCK_DGRAM
__in int protocol); IPPROTO_TCP 、IPPROTO_UDP 、0表示系统会自动推演出应该使用什么协议。
socke名称包括3个属性:1.协议 2.端口号 3.ip地址 所以使用bind实现其命名。服务端必须使用bind命名,客户端socket名称是可选的,可使用bind绑定也可以不绑定,尽量不绑定。bind属于显性绑定,但socket都需要一个完整的名称,如果不是显性命名,协议栈将隐式为其命名:指派本地ip地址和从用户定义的服务端口号分配一个,这样避免和其他socket冲突。
隐式命名在tcp 是connect函数 ,udp是sendto函数。
tcp协议下:
1、源端口号:发送方端口号
2、目的端口号:接收方端口号
3、序列号:报文段的数据的第一个字节的序号
3、确认序号:期望收到对方下一个报文段的第一个数据字节的序号
4、首部长度(数据偏移):TCP报文段的数据起始距离TCP报文段的起始处有多远,即首部长度
6、保留:保留不用是置为0
7、紧急URG:此置为 1 ,紧急指针字段才有效,它告诉系统此报文段中有紧急数据,应尽快传送
8、确认位ACK:此置为 1,确认号字段才有效,TCP规定,在连接建立后所有传达的报文段都必须把 ACK 置 1
9、推送位PSH:此置为 1,即发送方,希望接收方接收缓冲区的数据,即TCP使用推送(PUSH)操作,接收方不再等整个缓冲区填满后再交付
10、复位RST:用于复位相应的TCP连接
11、同步SYN:仅在三次握手建立TCP连接时有效,当SYN = 1 且 ACK = 0,表明 请求连接报文段,SYN = 1 且 ACK = 0,同意建立连接报文段
12、终止FIN:用来释放连接,FIN = 1,表明此报文段的数据发送已经发送完毕,并要求释放连接
13、窗口大小:指发送本报文段的一方的接受窗口(而不是自己的发送窗口),最大为65535字节。
14、校验和:校验字段检验的范围(包括首部和数据两部分),计算校验和时需要加上 12 字节的伪头部
15、紧急指针:仅在 URG = 1时才有意义,它代表本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据),即指出紧急数据在报文末尾的位置,(注意:及时窗口为0 时也可以发送紧急数据)
16、选项:长度可变,最长可达 40 字节,当没有使用选项时,TCP首部长度是 20 字节
Transmission Control Protocol, Src Port: http (80), Dst Port: 60575 (60575), Seq: 624361, Ack: 1, Len: 1452
Source Port: http (80) //源端口号
Destination Port: 60575 (60575) //目的端口号
[Stream index: 0]
[TCP Segment Len: 1452]
Sequence number: 624361 (relative sequence number) //32位序列号
[Next sequence number: 625813 (relative sequence number)]
Acknowledgment number: 1 (relative ack number) //32位确认序列号,即发送端希望收到的序列号
0101 .... = Header Length: 20 bytes (5) //4位首部长度
Flags: 0x010 (ACK) //标志位 ACK置1
000. .... .... = Reserved: Not set //保留位
...0 .... .... = Nonce: Not set //新增的
.... 0... .... = Congestion Window Reduced (CWR): Not set //新增的
.... .0.. .... = ECN-Echo: Not set //新增的
.... ..0. .... = Urgent: Not set
.... ...1 .... = Acknowledgment: Set
.... .... 0... = Push: Not set
.... .... .0.. = Reset: Not set
.... .... ..0. = Syn: Not set
.... .... ...0 = Fin: Not set
[TCP Flags: ·······A····]
Window size value: 240 //16位窗口大小(用于接收方的流量控制)
[Calculated window size: 240]
[Window size scaling factor: -1 (unknown)]
Checksum: 0xacb5 [unverified] //16位检验和
[Checksum Status: Unverified]
Urgent pointer: 0 //16位紧急指针
[SEQ/ACK analysis]
[Bytes in flight: 4356]
[Bytes sent since last PSH flag: 622908]
[Timestamps]
[Time since first frame in this TCP stream: 3.239693000 seconds]
[Time since previous frame in this TCP stream: 0.000001000 seconds]
TCP payload (1452 bytes) //TCP有效载荷
窗口大小就是无需等待应答,而可以继续发送数据的最大值。
这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
通常窗口的大小是由接收方的窗口大小来决定的。
接收窗口的大小是约等于发送窗口的大小的。
需要控制发送方和接收方的发送速率,这就叫做流量控制
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。为了解决这种死锁现象,只要任一方接收到了0窗口通知,就会启动一个计时器,如果计时器超时,就会发送窗口探测报文
,用于探测对方窗口大小。如果探测了三次还是0的话,就会发送RST报文终止连接。
MSS:最大报文段长度(MSS: Maximum Segment Size)表示TCP传往另一端的最大块数据的长度.MSS值通常设置为外出接口上的MTU长度减去固定的IP首部和TCP首部长度。对于一个以太网, MSS值可达1460字节(1500-20-20)。
只要缓存中存放的数据达到MSS字节时,就组成一个TCP报文段发送出去。当数据长度超过MSS值,就会根据MSS值分多次进行传输,叫做分段(与IP层分片相似)
不让接收方通告我有一个小窗口
当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。
在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大…
拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
慢启动
TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗?
慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
拥塞避免算法
慢启动门限概念
- 当cwnd
- 当cwnd>=ssthresh时,用拥塞避免算法
那么进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd
当网络中出现拥塞,也就是触发了重传机制,那么就会进入拥塞发生阶段
发生超时重传时,使用的拥塞发生算法如下,因为此时已经认为很拥塞了
这个时候,ssthresh 和 cwnd 的值会发生变化:
快速重传
如果是个别报文段会在网络中丢失,但实际上网络并未发生拥塞,则使用快速重传而不是超时重传。
发生快速重传时,使用的拥塞发生算法(快速恢复算法)如下
当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。
TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:
cwnd=cwnd/2
ssthresh=cwnd
快速恢复
然后,进入快速恢复算法如下:
拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
重传丢失的数据包
如果再收到重复的 ACK,那么 cwnd 增加 1;
如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
int socket( int af, int type, int protocol);
af:协议族。支持AF_INET格式(ipv4)和AF_INET6格式(ipv6)
type:指定socket类型。如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)
rotocol:就是指定协议,如调用者不想指定,可用0。常用的协议有,IPPROTO_TCP、IPPROTO_UDP。
返回值:若无错误发生,socket()返回引用套接字的描述字。否则的话,返回INVALID_SOCKET错误,应用程序可通过WSAGetLastError()获取相应错误代码。
服务器必须在一个已知的名称(ip地址、端口)监听,属于显性绑定。客户端不需要调用bind绑定端口,而是由内核自定选择临时端口,属于隐性绑定。
int
PASCAL FAR bind( SOCKET sockaddr,
const
struct
sockaddr FAR* my_addr,
int
addrlen);
sockaddr
表示已经建立的socket编号(描述符);
FAR
是一个指向sockaddr结构体类型的指针;
addrlen
表示my_addr结构的长度,可以用sizeof操作符获得。
ip地址使用的是网络字节顺序,所以需要调用htons、htonl 函数主机把ip地址和端口的主机字节序转换网络字节序。对服务器而言 对于IPv4来说,通配地址通常由INADDR_ANY来指定, 其值一般为0。它告知内核去选择IP地址。
如无错误发生,则bind()返回0。否则的话,将返回-1,应用程序可通过WSAGetLastError()获取相应错误代码。
常见的错误码WSAEADDRINUSE(地址已使用),可以通过调用setsockopt()函数设置SO_REUSEADDR选项。
如果tcp服务器没有把ip地址捆绑到套接字上,内核就把客户端发送的syn的目的ip地址作为服务器的源ip地址。服务器可以通过getsockname函数获取该地址。
在bind函数调用的sockaddr 类型指针,可以直接用前面的sockaddr_in变量转换替换。
iPv4通配地址由INADDR_ANY指定,如果端口号为0表示由内核选择一个临时端口。
套接字分为UDP套接字和TCP套接字,对于前者,其由(ip地址,端口号)来标识,后者由(源ip,源端口号,目的ip,目的端口号)标识。
tcp:可以的,如多个不同的客户端连接服务端,会产生多个套接字,这些套接字实际上是共用了相同的服务端端口号,但源ip地址不一样。
疑问:如果是多个套接字,或者是多个进程应用绑定在同一ip,端口上,会怎样?tcp、udp都可以吗?
默认的情况下,如果一个套接字 绑定了一个端口,这时候,别的套接字就无法使用这个端口。
但是端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。
具体操作是:设置socket的SO_REUSEADDR选项,即可实现端口复用:
int opt = 1;
// sockfd为需要端口复用的套接字
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));
SO_REUSEADDR可以用在以下四种情况下:
1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。
2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可以测试这种情况。
3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。这和2很相似,区别请看UNPv1。
4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。
它用于一个已捆绑或已连接套接字s,本地地址将被返回
int PASCAL FAR getsockname( SOCKET s, struct sockaddr FAR* name,
int FAR* namelen);
s:标识一个已捆绑套接口的描述字。
name:接收套接口的地址(名字)。
namelen:名字缓冲区长度。
若无错误发生,getsockname()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
获取与套接口相连的端地址。
#include
int PASCAL FAR getpeername( SOCKET s, struct sockaddr FAR* name,
int FAR* namelen);
s:标识一已连接套接口的描述字。
name:接收端地址的名字结构。
namelen:返回名字结构的长度。
int listen( int sockfd, int backlog);
sockfd:用于标识一个已捆绑未连接套接口的描述字。
backlog:等待连接队列的最大长度。
如无错误发生,listen()返回0。否则的话,返回-1,应用程序可通过WSAGetLastError()获取相应错误代码。
该函数应该在socket和bind函数之后,accept函数之前。套接字会从closed状态转换到listen状态。
SOCKET accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
默认是阻塞函数
sockfd:套接字描述符,该套接口在listen()后监听连接。
addr:(可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址(即客户端的地址)。Addr参数的实际格式由套接口创建时所产生的地址族确定。
addrlen:(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数。
返回值:如果没有错误产生,则accept()返回一个描述所接受包的SOCKET类型的值。否则的话,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetLastError()来获得特定的错误代码
参数的sockaddr 类型指针,可以直接用前面的sockaddr_in变量转换替换。该地址变量是输出参数,表示的是客户端的地址。
同时返回新的套接字,用于传输数据用。原来的套接字继续用来监听。这时候,注意3次握手的过程,还要考虑是否是阻塞,同步的状态。默认是阻塞。
accept()函数成功返回时,完成了关联的建立,即3次握手成功结束。
检测到达的连接请求:accept()函数调用成功,或者是select()函数指示监听socket上有可写数据。
默认是阻塞函数
buf缓冲区的类型都是char 最后一个变量flag 一般是0 在tcp中,要注意缓冲区不够,导致要么重发要么重收。所有要判断返回值。
int recv( _In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags);
返回值:
若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
int send( SOCKET s, const char FAR *buf, int len, int flags );
若无错误发生,send()返回所发送数据的总数(请注意这个数字可能小于len中所规定的大小,该值应该不可以为0)。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
只适合在tcp上,udp这函数没有意义。值关闭连接,不释放socket资源。
shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零
SHUT_RD:关闭连接的读端
SHUT_WR:关闭连接的写端
SHUT_RDWR:连接的读端和写端都关闭。 这与调用shutdown两次等效。第一次调用指定SHUT_RD,第二次调用指定SHUT_WR
4次握手,该函数默认是非阻塞函数。只有对一个阻塞socket,并且调用setsockopt设置了非0的超时值来使能SO_DONTLINGER。它才是阻塞的。
int PASCAL FAR closesocket( SOCKET s);
如无错误发生,则closesocket()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
在linux中关闭套接字使用的函数是close().
close把描述符的引用计数减1,仅在该计数为0时才关闭套接字,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写。
close终止读写两个方向的数据传输。
可以通过setsockopt函数设置SO_DONTLINGER 、SO_LINGER选项。
int connect(SOCKET s, const struct sockaddr * name, int namelen);
s:标识一个未连接socket
name:指向要连接套接字的sockaddr 结构体的指针
namelen:sockaddr结构体的字节长度
返回值:成功则返回0, 失败返回-1.
成功后,套接字的状态从closed->syn_sent ->established.
注意阻塞函数,握手3步.connect函数由系统决定超时时间,一般是75s.
失败的多种原因:
1.具体流程是:发送一个syn,若无响应的等待几秒再发送一个,连续好几次,等到75s仍不响应。
在windows 则返回SOCKET_ERROR(也即-1).调用WSAGetLastError(),返回出错码是WSAETIMEDOUT。
在linux 则返回-1.通过全局变量errno,返回出错码是ETIMEDOUT。
2.若对客户端返回的是RST,表明在服务器主机指定的端口没有进程在等待与之连接。
出错代码是WSAECONNREFUSED(windows),ECONNREFUSED(linux)。
3.中间某个路由引发icmp错误。按照1方式联系75s发送syn包,错误码是: WSAENETUNREACH
所以需要把套接字改成非阻塞模式、并且使用select函数,自定义超时时间。可参考下面的做法。
调用的sockaddr 类型指针,可以直接用前面的sockaddr_in变量转换替换。需要知道服务端的ip地址和端口。
注意:客户端没必要调用bind进行绑定命名,主要原因是:
如果没事前调用bind()函数,connect()函数会隐式对本地socket命名,并且是任取一个端口,避免端口冲突。
同时,如果同时运行多个客户端,给套接字指定端口容易导致端口冲突。
如果调用connect失败,推荐先调用closesocket()和socket()获取一下新的socket。
buf缓冲区的类型都是char 最后一个变量flag 一般是0
正常关闭只出现在面向连接中,如tcp协议。双方都执行一次关闭,才能完全中断连接。
关于 send和recv即发送和接收的理解:
send函数:
只负责将数据提交给网络协议层。 当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲区的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR; 如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据; 如果是,就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len; 如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完,如果len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里
注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)
send()只是负责拷贝,拷贝完立即返回,不会等待发送和发送之后的ACK。如果发送数据的过程中出现各种错误,下一次send()或者recv()调用的时候被立即返回。
recv();
recv()先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中。
TCP协议中,接收方成功接收到数据后,会回复一个ACK数据包,表示已经确认接收到ACK确认号前面的所有数据。
发送方在一定时间内没有收到服务端的ACK确认包后,就会重新发送TCP数据包。
接收方在接收到数据后,不是立即会给发送方发送ACK的。这可能由以下原因导致:
1、收到数据包的序号前面还有需要接收的数据包。因为发送方发送数据时,并不是需要等上次发送数据被Ack就可以继续发送TCP包,而这些TCP数据包达到的顺序是不保证的,这样接收方可能先接收到后发送的TCP包(注意提交给应用层时是保证顺序的)。
2、为了降低网络流量,ACK有延迟确认机制。
3、ACK的值到达最大值后,又会从0开始。
接收方在收到数据后,并不会立即回复ACK,而是延迟一定时间。一般ACK延迟发送的时间为200ms,但这个200ms并非收到数据后需要延迟的时间。系统有一个固定的定时器每隔200ms会来检查是否需要发送ACK包。这样做有两个目的。
1、这样做的目的是ACK是可以合并的,也就是指如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以降低网络流量。
2、如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息。这样做,可以避免大量的ACK以一个单独的TCP包发送,减少了网络流量。
MSS 是在建立连接时通过SYN数据包中的MSS选项里进行协商的(以太网的MTU能到1500,所以MSS可以为1460),如果没有协商,默认为536,MSS是数据净负荷,协议保证最小支持536(加上TCP和IP的头部后packet为576)
TCP中在发送的数据的ACK未回来前,能继续发送其他数据包吗
能不能发,取决于下面的条件是否满足:
1. 如果包长度达到MSS,则再根据CWND、AWND来做决定;
2. 如果该包含有FIN,则允许发送;
3. 如果没达到MSS且不包含FIN:
3.1. 设置了TCP_NODELAY选项,则允许发送;
3.2. 没设置TCP_NODELAY, 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送(nagel算法起作用);设置了TCP_CORK选项时,需要包长度到MSS。
int on = 1;
setsockopt(fd,SOL_TCP,TCP_CORK,&on,sizeof(on));
4. 上述条件都未满足,但发生了超时(一般为200ms),则立即发送
这里需要指出的一点是,伪首部完全是虚拟的,它并不会和用户数据报一起被发送出去,只是在校验和的计算过程中会被使用到,伪首部主要来自于运载UDP报文的IP数据报首部,将源IP地址和目的IP地址加入到校验和的计算中可以验证用户数据报是否已经到达正确的终点。
所以udp头部大小为8字节。
第一行:
源端口号16bit+目的端口号16bit
第二行:
UDP长度16bit+UDP校验和16bit
第三行:
发送数据部分
1.创建,socket()函数
2.绑定, bind()函数
3.接收 recvfrom
不用实际读取就能检测到数据的到来,可以调用MSG_PEEK标志的recv活recvfrom,或者调用ioctlsocket或select.
ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);
sockfd:标识一个已连接套接字的描述字。
buf:接收数据缓冲区。
len:缓冲区长度。
flags:调用操作方式。一般为0, 是一个或者多个标志的组合体,可通过“ | ”操作符连在一起:
from:指针,指向装有源地址的缓冲区。可选,如不关心,则为null
fromlen:指针,指向from缓冲区长度值。可选,如不关心,则为null
返回值
如果正确接收返回接收到的字节数,失败返回-1.返回0也是可行的。tcp的read返回0表示对端已关闭,而udp不是。
udp和tcp都有接收缓冲区,在没有收到数据时,默认是阻塞的,可通过设置超时返回(如select机制,默认最大描述符为1024)
延伸connect
1.没有3路握手,只是检查是否存在立即可知的错误(如网络不可达,但如果是网络可达,但应用程序没启动检查不到的,只有调sendto才返回)
2.如果udp调用connect后,不能使用sendto(或者不能在sendto制定目的地址)
3.如果udp调用connect后,只能接受connect所指定地址的数据报,不能接受别的套接字的数据。
相当于不能进行广播和组播了
4.好处:就是只显性连接一次,可以多次发送数据。传输效率更高。
不然调用sendto就是连接一次,发送一次,断开连接一次。再连接,发送,断开连接
5.多次调用connect,可以指定新的目的地址和端口和断开套接字
关闭closesocket() :
只是将socket的资源归还给协议栈。
1.创建,socket()函数
2.发送 sendto ()函数:
适合在从同一个socket向不同的远程主机发送数据。
或还有一种办法是,调用connect()函数,再调用send()函数。适用于向同一个远程地址发送数据。
int sendto ( socket s , const void * msg, int len, unsigned int flags, const struct sockaddr * to , int tolen ) ;
send 和 sendto 函数在 UDP 层没有输出缓冲区,因此sendto不会阻塞。而是直接返回。
s 套接字
buff 待发送数据的缓冲区
size 缓冲区长度
Flags 调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式
addr (可选)指针,指向目的套接字的地址
len addr所指地址的长度
成功则返回实际传送出去的字符数,失败返回-1,返回是0也是可以的。
udp并没有真正的发送缓冲区,和tcp还是有区别的。
对客户端的udp而言,进程首次调用sendto时,绑定一个临时端口,不能在修改了。而ip地址可以随客户发送的udp数据报而变动。
udp sendto输出操作成功返回仅仅表示在接口输出队列中具有存放所形成ip数据报的空间。
说明了udp套接字:由它引发的异步错误并不返回给它,除非它也连接(即如调用了connect函数)。这也是udp使用connect的初衷。
主要区别在于:udp获取目标ip地址的方法是recvmsg函数。
在客户端udp中,调用connect并没有和tcp的三路握手,只能是内核检查存放立即可知的错误,记录对端的ip地址和端口,然后立即返回到进程。如果使用了connect函数,就只能使用read(recv)替代recvfrom ,write(send)替代sendto.
udp可以多次调用connect,其目的是指定新的ip地址和端口,或断开套接字。tcp只能一次调用connect。
调用connect并不给服务器发送任何信息,只是保存对端的ip地址和端口号。
tcp和udp可以同用一个端口。
对于已连接的udp套接字可以调用sendto,但不能指定目的地址。
对于已连接的udp套接字只能通过connect断开连接,而不能通过shutdown.
3.关闭closesocket
主要在udp中,可以扩展,从单播到组播,或广播。可以在同一应用程序同时接收和发送。
udp套接字显性地绑定一个本地ip接口,并发送数据。出现什么情况
udp套接字不会真正和网络接口绑定在一起,而是建立一种关联,即被绑定的ip接口地址成为发出去的udp数据报的源ip地址。
一个数据报即udp只要一打开就处于可写的状态,一经命名就处于可读的状态。
所以,应用程序下socket()调用后,马上可以发送数据。
在调用bind()显式命名或调用sendto函数隐式命名后,马上就可以接收数据。
对于udp多播,发送应用进程的套接字可以不必加入到多播组。
一直都UDP层(通过端口号),才能确定是否要丢弃广播数据。
发送端:
1.创建udp套接字,无须绑定端口和地址。
2.设置udp套接字的广播标志。
int flag = 1;
setsockopt(server_sockfd , SOL_SOCKET , SO_BROADCAST , &flag , sizeof(flag) );
3.调用sendto函数,参数中需要明确广播地址和端口号。
#define BROADCAST_IP "192.168.1.255"
#define CLIENT_PORT 9000
bzero(&clientaddr , sizeof(clientaddr));
clientaddr.sin_family = AF_INET;
inet_pton(AF_INET , BROADCAST_IP , &clientaddr.sin_addr.s_addr);
clientaddr.sin_port = htons(CLIENT_PORT);
sendto(server_sockfd , buf , strlen(buf) , 0 , (struct sockaddr *)&clientaddr , sizeof(clientaddr));
接收端:
1.创建udp套接字,必须绑定端口(该端口是发送端发送的端口)。绑定的IP不可以使用“127.0.0.1”,可以使用真实IP地址或者INADDR_ANY。否则接收失败。
bzero(&localaddr , sizeof(localaddr));
localaddr.sin_family = AF_INET;
inet_pton(AF_INET , "0.0.0.0" , &localaddr.sin_addr.s_addr);
localaddr.sin_port = htons(CLIENT_PORT);
int ret = bind(confd , (struct sockaddr *)&localaddr , sizeof(locala ddr));
2.接收方的Socket不需要设置成广播属性。
3.调用recvfrom函数进行接收.
len = recvfrom(confd , buf , sizeof(buf),0 ,(struct sockaddr*)&from,(socklen_t*)&len);
//from为发送端的本地地址。
相关的广播知识点:
BOOL bBroadcast=TRUE;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(BOOL));
// 接收缓冲区
int nRecvBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
//发送缓冲区
int nSendBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
//地址复用
BOOL bReuseaddr = TRUE;
setsockopt( s, SOL_SOCKET, SO_REUSEADDR, ( const char* )&bReuseaddr, sizeof( BOOL ) );
特点:
1.广播的数据在子网的所有主机都接收。
2.不能够跨越不同的网络,被路由器所隔离开,即只能在局域网,不能应用到广域网
3.接收端的端口号要与广播端绑定的端口号一样
组播在网卡处就对接收地址进行判断,从而丢弃数据包。
一个ip地址可以加入到多个组播组。
1.地址范围:D类IP地址。范围:224.0.0.0~239.255.255.255
2.组播组:永久/临时。永久组播组一般由官方分配。
3.224.0.0.0~224.0.0.255为预留的组播地址,即永久组地址。地址224.0.0.0保留不做分配,其它地址供路由协议使用。
4.224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet。
5.224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效。
6.239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。
发送端:
1.创建udp套接字,无须绑定端口和地址。
2.调用sendto函数,参数中需要明确组播地址和端口号。
#include
#include
#include
#include
#include
#include
#include
int main()
{
int server = 0;
struct sockaddr_in saddr = {0};
int client = 0;
struct sockaddr_in remote = {0};
socklen_t asize = 0;
int len = 0;
char buf[32] = "Software";
int r = 0;
//int brd = 1;
server = socket(PF_INET, SOCK_DGRAM, 0);
if( server == -1 )
{
printf("server socket error\n");
return -1;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY); // 本机地址
saddr.sin_port = htons(8888);
if( bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1 )
{
printf("udp server bind error\n");
return -1;
}
printf("udp server start success\n");
remote.sin_family = AF_INET;
remote.sin_addr.s_addr = inet_addr("224.1.1.168"); //设置一个多播地址
remote.sin_port = htons(9000);
while( 1 )
{
len = sizeof(remote);
r = strlen(buf);
sendto(server, buf, r, 0, (struct sockaddr*)&remote, len);
sleep(1);
}
close(server);
return 0;
}
接收端:
1.创建套接字,必须绑定端口(该端口是发送端发送的端口)。绑定的IP不可以使用“127.0.0.1”,可以使用真实IP地址或者INADDR_ANY。否则接收失败。
2.把当前本地的ip地址加人到组播地址。
3.调用recvfrom获取数据,参数为发送端的本地地址。
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sock = 0;
struct sockaddr_in addr = {0};
struct sockaddr_in remote = {0};
int len = 0;
char buf[128] = {0};
char input[32] = {0};
int r = 0;
//多播
struct ip_mreq group={0};
sock = socket(PF_INET, SOCK_DGRAM, 0);
if( sock == -1 )
{
printf("socket error\n");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(9000);
if( bind(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1 )
{
printf("udp bind error\n");
return -1;
}
//remote.sin_family = AF_INET;
//remote.sin_addr.s_addr = inet_addr("127.0.0.1");
//remote.sin_port = htons(8888);
group.imr_multiaddr.s_addr=inet_addr("224.1.1.168");
group.imr_interface.s_addr=htonl(INADDR_ANY); //local host
//这里INADDR_ANY 为0.0.0.0 通过看ipconfig/ifconfig 可以看到有多个
//网络ip地址,这个时候让操作系统选择哪一个端口进行多播数据收发。
//在实际的工程中需要明确指定需要哪一个网络地址进行多播数据收发,
//不能完全依赖操作系统,否者有时候能够收到数据,有时候收不到数据。
setsockopt(sock,IPPROTO_IP,IP_ADD_MEMBERSHIP,&group,sizeof(group));
while( 1 )
{
len=sizeof(remote);
r = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)&remote, &len);
if( r > 0 )
{
buf[r] = 0;
printf("Receive: %s\n", buf);
}
else
{
break;
}
}
close(sock);
return 0;
}
多播(组播)相关知识点:
ip多播:采用的是“无根”的通讯方式,组内的成员可以相互之间发送和接收数据。IPv4 多播是一个D类的IP地址,范围:224.0.0.0 ----239.255.255.255 之间。
如果多播需要在多个网络断传输,需要考虑使用IGMP协议,该协议运行在主机和组播路由器之间。此外一个套接字注意TTL的值,其作用主要显示数据能传输多远。
1.多播服务端针对特定多播地址只发送一次数据,但是组内的所有客户端都能收到数据
也就是说如果自身不想接受数据,就不要把自己加入到组播地址去。
2.加入特定的多播组即可接收发往该多播组的数据。如果自己想接收数据,就把自己加入组播地址
3.与单播一样,多播是允许在广域网即Internet上进行传输的,而广播仅仅在同一局域网上才能进行;即可以跨网段,跨路由器.所以多播时,路由器能够复制数据并进行转发
常用函数
IPPROTO_IP
当接收者加入到一个多播组以后,再向这个多播组发送数据,这个字段的设置是否允许再返回到本身。
int loop=1; //1:on 0:off
setsockopt(sock,IPPROTO_IP,IP_MULTICAST_LOOP,&loop,sizeof(loop));
IP_MULTICAST_TTL
默认情况下,多播报文的TTL被设置成了1,也就是说到这个报文在网络传送的时候,它只能在自己所在的网络传送,当要向外发送的时候,路由器把TTL减1以后变成了0,这个报文就已经被Discard了。
IP_MULTICAST_IF
设置多播接口地址
struct in_addr addr;
setsockopt(s,IPPROTO_IP,IP_MULTICAST_IF,&addr,sizeof(addr))
IP_ADD_MEMBERSHIP
加入一个组播组
struct ip_mreq ipmr;
ipmr.imr_interface.s_addr = inet_addr("192.168.101.1");
ipmr.imr_multiaddr.s_addr = inet_addr("234.5.6.7");
setsockopt(s, IPPROTO_IP, IP_ADDR_MEMBERSHIP, (char*)&ipmr, sizeof(ipmr));
IP_DROP_MEMBERSHIP
离开一个多播组
tcp和udp可以同用一个端口。
套接字超时
1.使用select阻塞等待
2.使用SO_RCVTIMEO和SO_SNDTIMEO 套接字选项setsockopt。
这两种办法适用于recv、send。例外select使用与connect前提条件是套接字是非阻塞。选项不适用于connect。
在linux系统可以使用alarm函数,从产生SIGALRM信号。
socket状态检测方法
1.根据函数调用成功或失败检测
2.同步检测(select()、ioctlsocket()、getsockopt()和io标志)
3.异步通知 最有效
如下例:
1.当recv()返回值小于等于0时,socket连接断开。但是还需要判断 errno是否等于 EINTR,如果errno == EINTR 则说明recv函数是由于程序接收到信号后返回的,socket连接还是正常的,不应close掉socket连接。
2.getsockopt:
struct tcp_info info;
int len=sizeof(info);
getsockopt(sock, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len);
if((info.tcpi_state==TCP_ESTABLISHED)) 则说明未断开 else 断开
3.select
若使用了select等系统函数,若远端断开,则select返回1,recv返回0则断开.其他注意事项同法一 页就是说还需进一步判断。
4.该方法更多用于服务器,
int keepAlive = 1; // 开启keepalive属性
int keepIdle = 60; // 如该连接在60秒内没有任何数据往来,则进行探测
int keepInterval = 5; // 探测时发包的时间间隔为5 秒
int keepCount = 3; // 探测尝试的次数.如果第1次探测包就收到响应了,则后2次的不再发.
setsockopt(rs, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive));
setsockopt(rs, SOL_TCP, TCP_KEEPIDLE, (void*)&keepIdle, sizeof(keepIdle));
setsockopt(rs, SOL_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
setsockopt(rs, SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));
设置后,若断开,则在使用该socket读写时立即失败,并返回ETIMEDOUT错误
ip4地址寻址
是32位数,可以用点分十进制格式 ,如192.168.101.1/24 /后面 表示子网掩码的位数 如 24 表示 和255.255.255.0进行或运算
有 A类(0-127)、B类(128-191)、C类(192-223)、D类(224-239) 、E类(240-255)地址。
多播(组播)是D类地址,
端口:
0-1023 :为已知服务保留的
1024-49151:普通用户可使用,一般使用在范围的
49152-65535:动态和专用端口
字符串文字地址和套接字地址之间转换函数:
WSAStringToAddress 和 WSAAddressToString
此外还有getaddrinfo 和getnameinfo
套接字选项和io控制
1.getsockopt
2.setsockopt 用于任意类型、任意状态套接口的设置选项值
一种是布尔型选项,允许或禁止一种特性;另一种是整形或结构选项。允许一个布尔型选项,则将optval指向非零整形数;禁止一个选项optval指向一个等于零的整形数
int PASCAL FAR setsockopt( SOCKET s, int level, int optname,
const char FAR* optval, int optlen);
s:标识一个套接口的描述字。
level:选项定义的层次;目前支持SOL_SOCKET和IPPROTO_TCP、IPPROTO_IP 层次。 一般是使用SOL_SOCKET
optname:需设置的选项。
optval:指针,指向存放选项值的缓冲区。
optlen:optval缓冲区的长度。
选项 类型 意义
SOL_SOCKET
SO_BROADCAST BOOL 允许套接口传送广播信息。 适用范围是:非sock_stream 类型
SO_RCVBUF int 为接收确定缓冲区大小。
SO_SNDBUF int 指定发送缓冲区大小
SO_REUSEADDR BOOL 允许套接字和一个已在使用中的地址捆绑 ,目的就是地址复用,如广播地址和组播地址使用同一个套 接字
缺省条件下,一个套接字不能与一个已在使用中的本地地址捆绑。
SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。
SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。对于TCP,我们根本不可能启动捆绑相同IP地址和相同端口号的多个服务器。
SO_REUSEADDR允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。
SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。
SO_LINGER 选项 linux也是一样的
SO_LINGER struct linger FAR* 如关闭时有未发送数据,则逗留。
//逗留,具体程序要求待未发送完的数据发送出去后再关闭socket
struct linger m_sLinger;
m_sLinger.l_onoff = 1; //在调用close(socket)时还有数据未发送完,允许等待
// 若m_sLinger.l_onoff=0;则调用closesocket()后强制关闭
m_sLinger.l_linger = 5; //设置等待时间为5秒
setsockopt( s, SOL_SOCKET, SO_LINGER, (const char*)&m_sLinger, sizeof(struct linger);
非阻塞套接字
虽然已经SO_LINGER(即linger结构中的l_onoff域设为非零)且设为非零值时,closesocket但还是立即返回,closesocket()调用将以WSAEWOULDBLOCK错误返回。
阻塞套接字
1.已经设置SO_LINGER(即linger结构中的l_onoff域设为非零)且设为零值时,则closesocket()不被阻塞立即执行,不论是否有排队数据未发送或未被确认,且丢失了未发送的数据,给对端发送RST,b避免了time_wait状态,但可能导致在2msl内创建一个新的连接,旧的重复分节还在。在远端的recv()调用将以WSAECONNRESET出错。
2已经设置SO_LINGER(即linger结构中的l_onoff域设为非零)并确定了非零的超时间隔,则closesocket()调用阻塞进程,直到所剩数据发送完毕或超时。如果超时了,则closesocket()调用将以WSAEWOULDBLOCK错误返回。
3.设置了SO_DONTLINGER(也就是说将linger结构的l_onoff域设为零);则closesocket()调用立即返回,但是,如果可能,排队的数据将在套接口关闭前发送。所以还是要比直接调用closesocket要等待一些时间的。
3.ioctrlsocket
int ioctlsocket( int s, long cmd, u_long * argp);
常见的操作命令
FIONBIO:允许或禁止套接字的非阻塞模式。 1表示允许 0 表示禁止
FIONREAD:
SIOCATMARK:
int iBlocking = 1;
ioctlsocket( sock,FIONBIO, (unsigned long FAR*)&iBlocking );
阻塞模式
缺点:延伸性不好,不可以使用太多套接字。
需要使用的知识点:
1.开线程监听
2.每一个连接开线程用于接收或发送数据。
3.使用数组管理套接字和线程。
4.处理数据考虑线程同步问题,使用临界区关键段和事件量,再结合waitingforsingleobject。
阻塞操作中的超时
1.自动超时:如connect、send函数,由网络系统单独决定 一般超时时间为75s
2.用户可设置的超时:select、closesocket允许应用程序来决定超时值
3.应用程序超时:recv、recvfrom、accept函数能够永远阻塞下去。
4.tcp存活超时。流socket提供setsockopt选项SO_KEEPALIVE。
非阻塞模式
1 注意返回值是WSAEWOULDBLOCK, 至少说明函数调用没有失败,一般通过再一次调用。
2.部分成功。
非阻塞模式下connect 成功失败判断
1.将打开的socket设为非阻塞的,可以用int iBlocking = 1; ioctlsocket( sock,FIONBIO, (unsigned long FAR*)&iBlocking );实现。
2.发connect调用, 如果是0,表示成功连接。如果这时返回-1(即SOCKET_ERROR)但是errno被设为EINPROGRESS,意即connect连接已经建立但还没有完成.
3.将打开的socket设进被监视的可写(注意不是可读)文件集合用select进行监视, 即if
(select(0, NULL, &writefds, null, &timeout) > 0) 。
如果可写用getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int)); 来得到error的值,如果为零,则connect成功.
建议accept也设置成非阻塞模式使用
项目数据管理
1.开一个监听线程。
2.每一个客户端连接,开一个线程来接收数据、处理数据、发送数据。这是最简单的方式,只用到线程、数组。甚至都不用进行线程同步。
int select(
int nfds,//忽略,只是为了保持与早期的Berkeley套接字应用程序的兼容 ,设为0即可。
但在linux系统中是最大描述符加1,说明需要事先知道最大描述符是多少(默认最大描述符为1024)。
fd_set FAR* readfds, //可读或等待关闭 可读具体一般表现为有连接和接收数据
fd_set FAR* writefds,// //可写或已连接 可写性检查(即有数据可发出)
fd+set FAR* exceptfds,//带外数据检查(带外数据),为空则不检查
const struct timeval FAR* timeout//超时 无限等待为null, 根本不等待为0。
);
readfds、writefds、exceptfds,如果不感兴趣,就把它设置为null.
如果三个都为null,就有一个比sleep精度更高的定时器了,该方法也可行。
返回值:
0 :表示超时 ,说明readfds、writefds、exceptfds状态不确定,需重新初始化。没有描述符准备好。
-1 即 SOCKET_ERROR 表示失败
>0 :成功 当前状态与设定状态相匹配的socket的个数,如果同一个描述符已准备好读和写,那么在返回值中会对其计数两次。
只检测socket状态,不提供io操作。是一个同步函数。
当timeout 超时值非零时,select()函数是一个阻塞函数。
通过采用非阻塞的socket和阻塞的select函数来更有效地复用多个socket.
目的:
1,防止在套接字处于阻塞模式中,调用send或recv进入阻塞状态
2.防止在套接字处于非阻塞模式中,产生WSAEWOULDBLOCK错误。
优点:减少线程的开销。
缺点:
1.每次调用select,都需要把监听的文件描述符集合fd_set从用户态拷贝到内核态,从算法角度来说就是O(n)的时间开销。
2.每次调用select调用返回之后都需要遍历所有文件描述符,判断哪些文件描述符有读写事件发生,这也是O(n)的时间开销。
3.内核对被监控的文件描述符集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)。
writefds包括符合以下任一个条件的套接字:
1.有数据可以发出
2.如果一个非阻塞connect连接请求正在被处理,并且连接已经成功 //怎样理解,看到有些书原文不是这个??
3.该连接的写半部关闭。在linux系统,这样的套接字写操作会产生SIGPIPE信号。
readfds包括符合以下人一个条件的套接字:
1.有数据可以读入
2.连接已经被关闭、重启或终止。
如果是关闭,套接字可读,会接收到fin,并read返回0.
如果是服务器崩溃并重启的话,套接字可读,会收到rst,并read返回-1。
3.假如已调用了listen,而且有一个连接正处于搁置状态,那么accept函数调用就会成功。
void FD_SET(int fd, fd_set *set); //在set中设置文件描述符fd
void FD_CLR(int fd, fd_set *set); //清除set中的fd位
int FD_ISSET(int fd, fd_set *set); //判断set中是否设置了文件描述符fd
void FD_ZERO(fd_set *set); //清空set中的所有位(在使用文件描述符集前,应该先清空一下)
select() 和 poll() 系统调用的本质一样,poll() 的机制与 select() 类似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
所以,TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活。
缺点:keepalive 会增加网络负荷。
int keepAlive = 1; // 开启keepalive属性
int keepIdle = 60; // 如该连接在60秒内没有任何数据往来,则进行探测
int keepInterval = 5; // 探测时发包的时间间隔为5 秒
int keepCount = 3; // 探测尝试的次数.如果第1次探测包就收到响应了,则后2次的不再发.
setsockopt(rs, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive));
setsockopt(rs, SOL_TCP, TCP_KEEPIDLE, (void*)&keepIdle, sizeof(keepIdle));
setsockopt(rs, SOL_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
setsockopt(rs, SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));
设置后,若断开,则在使用该socket读写时立即失败,并返回ETIMEDOUT错误
产生RST的原因
1. 访问不存在的端口。
若端口不存,则直接返回RST,同时RST报文接收通告窗口大小为0.其实客户端向服务器的某个端口发起连接,如果端口被处于TIME_WAIT 状态的连接占用时,客户端也会收到RST。这种情况很常见。特别是服务器程序core dump之后重启之前连续出现RST的情况会经常发生。
2. 异常终止连接。
一方直接发送RST报文,表示异常终止连接。
一旦发送方发送复位报文段,发送端所有排队等待发送的数据都被丢弃。应用程序可以通过socket选项SO_LINGER来发送RST复位报文。而接收端收到RST包后,不必发送ACK包来确认。
接收方如果不接受完所有的数据,就关闭链接,底层TCP会给发送方返回一个RET报文,表示数据没有被完全接受,并且关闭里。参考文章3有Demo实现。
3.处理半打开连接。
一方关闭了连接,另一方却没有收到结束报文(如网络故障),此时另一方还维持着原来的连接。而一方即使重启,也没有该连接的任何信息。这种状态就叫做半打开连接。而此时另一方往处于半打开状态的连接写数据,则对方回应RST复位报文。此时会出现connect reset by peer错误。
4.请求超时
设置 connect_timeout,应用设置了连接超时,sync 未完成时超时了,会发送rst终止连接。
曾经遇到过这样一个情况:一个客户端连接服务器,connect返回-1并且error=EINPROGRESS。 直接telnet发现网络连接没有问题。ping没有出现丢包。用抓包工具查看,客户端是在收到服务器发出的SYN之后就莫名其妙的发送了RST。
5,keepalive 超时
公网服务tcp keepalive 最好别打开;移动网络下会增加网络负担,切容易掉线;非移动网络核心ISP设备也不一定都支持keepalive,曾经也发现过广州那边有个核心节点就不支持。
6, TIME_WAIT 状态
tw_recycle = 1 时,sync timestamps 比上次小时,会被rst。
检测办法:改变socket的keepalive选项,以使socket检测连接是否中断的时间间隔更小,以满足我们的及时性需求。
所有的重传机制都是依赖于序列号seq以及确认应答ACK。
在使用TCP进行数据传输时每个消息都有带有TCP协议的包头,在包头中就存在seq序列号和ACK确认包。
seq(Sequence Number):32bits,表示这个tcp包的序列号。tcp协议拼凑接收到的数据包时,根据seq来确定顺序,并且能够确定是否有数据包丢失。
ack(Acknowledgment Number):32bits,表示这个包的确认号。首先意味着已经收到对方了多少字节数据,其次告诉对方接下来的包的seq要从ack确定的数值继续接力。
在三次握手中:
起始包的seq都等于0
三次握手中的ack=对方上一个的seq+1
seq等于对方上次的ack号
常见的重传机制主要有以下几种:
超时重传
快速重传
SACK
D-SACK