一、基本概念
socket:
翻译为插座,在网络编程中引申为“网络数据传输用的软件设备”。创建一个socket可以类比为安装一部电话机(都是通信用的)。
C/S模式:
- 即客户机和服务器模式,该模式在操作中采取主动请求的方式;
- 服务器端:
2.1. 首先服务器启动,并根据请求提供相应的服务;
2.2. 打开一个通信通道,在某一地址和端口上接收请求;
2.3. 等待客户端请求到达该端口;
2.4. 接收到重复服务请求,处理该请求并发送应答信号;
2.5. 返回第2步,继续等待其他客户端请求;
2.6. 关闭服务器; - 客户端:
3.1. 打开一个通信通道,连接到服务器主机的指定端口;
3.2. 向服务器发送服务请求,等待并接收应答,继续发送请求;
3.3. 请求结束后关闭通信通道,并终止;
面向连接与面向消息:
- 即TCP与UDP;
- 面向连接的套接字(TCP):
2.1. 传输过程数据不会丢失;
2.2. 传输过程会按照顺序传输;
2.3. 传输过程不存在数据边界; - 面向消息的套接字(UDP):
3.1. 强调快速传输,而并非顺序传输;
3.2. 传输的数据可能丢失,也可能被破坏;
3.3. 限制每次传输数据的大小;
3.4. 传输过程存在数据边界;
基本函数:
基本数据结构:
struct sockaddr_in {
short sin_family; // 2字节 地址类型 IPV4还是IPV6
u_short sin_port; // 2字节 端口号,最大为65535
struct in_addr sin_addr; // 4字节 IP地址
char sin_zero[8]; // 8字节 填充
};
二、流程
TCP基本流程:
服务端和客户端初始化 socket,得到文件描述符;
服务端调用 bind,将 socket 绑定在指定的 IP 地址和端口;
服务端调用 listen,进行监听;
服务端调用 accept,等待客户端连接;
客户端调用 connect,向服务端端的地址和端口发起连接请求;
服务端 accept 返回用于传输的 socket 的文件描述符;
客户端调用 write 写入数据;服务端调用 read 读取数据;
客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。
这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据
所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
UDP基本流程:
三、示例
例子代码,很简单的一个连接通话流程,有助理解:
TCPDemo:
链接:https://pan.baidu.com/s/19sCHawMUcTljK17UTJUXHA
提取码:ppnh
UDPDemo:
链接:https://pan.baidu.com/s/10A7YoutbKU1tFjuE2QJNZQ
提取码:1blt
四、其他
listen参数意义:
TCP的流程中有一步为listen,共有两个参数,分别为服务器套接字,以及最大监听的队列大小。
listen(
_In_ SOCKET s,
_In_ int backlog
);
主要说下第二个参数的意义:从listen开始到accept之间,最大监听数量为n个,即这段时间内,从第n+1个客户端开始是会连接错误的,错误码10061,而监听队列中的n个是不会有影响的 直到队列中的n个开始被陆续的处理,才可以空出位置给后面的客户端。虽然这样,但并不是n越大越好。
大数据的发送与接收:
在传输数据的过程中,肯定会有大数据传输的情况,这种情况该如何处理?上代码:
int MySocketRecv(int iSockFrom, char* buffer, int iDataLen)
{
int iReadedLen = 0; // 目前已经收到的数据大小
int iRemainingLen = iDataLen; // 剩余要接收的数据大小
while (1)
{
// 从目前已经收完的地方继续向后读取,读取大小为剩余未接收数据的大小,返回实际读取出数据的大小
int iCurReadLen = recv(iSockFrom, &buffer[iReadedLen], iRemainingLen, 0);
printf("本次读取数据大小:[%d],共读取数据大小:[%d],剩余未读取数据大小:[%d]\n",
iCurReadLen, iReadedLen + iCurReadLen, iRemainingLen - iCurReadLen);
if (iCurReadLen == iRemainingLen)
{
printf("数据全部接收完成\n");
return 0;
}
else if (iCurReadLen > 0)
{
iReadedLen += iCurReadLen;// 累加上读取出来的大小
iRemainingLen -= iCurReadLen;// 累减去读取出来的大小
continue;// 进入下一次读取
}
else if (iCurReadLen < 0 && errno == 11)
continue;
else
return -1;
}
}
int MySocketSend(int iSockTo, char* buffer, unsigned iLen)
{
unsigned iSendedLen = 0;// 目前已发送数据大小
unsigned iRemainingLen = iLen;// 剩余未发送数据大小
while (1)
{
// 从目前已经发送的地方继续向后发送,发送大小为剩余未发送数据的大小,返回实际发送数据的大小
int iCurSendLen = send(iSockTo, &buffer[iSendedLen], iRemainingLen, 0);
printf("本次发送数据大小:[%d],共发送数据大小:[%d],剩余未发送数据大小:[%d]\n",
iCurSendLen, iSendedLen + iCurSendLen, iRemainingLen - iCurSendLen);
if (iCurSendLen == iRemainingLen)
{
printf("数据全部发送完成\n");
return 0;
}
else if (iCurSendLen > 0)
{
iSendedLen += iCurSendLen;
iRemainingLen -= iCurSendLen;
continue;
}
else if (iCurSendLen < 0 && errno == 11)
continue;
else
return -1;
}
}
即采用循环发送/接收的思想。
五、TCP三次握手
图示
TCP首部格式
源端口:占16比特,写入源端口号,用来标识发送该TCP报文段的应用进程。
目的端口:占16比特,写入目的端口号,用来标识接收该TCP报文段的应用进程。
序号:占32比特,取值范围[0,2^32-1],序号增加到最后一个后,下一个序号就又回到0。指出本TCP报文段数据载荷的第一个字节的序号。
确认号:占32比特,取值范围[0,2^32-1],确认号增加到最后一个后,下一个确认号就又回到0。指出期望收到对方下一个TCP报文段的数据载荷的第一个字节的序号,同时也是对之前收到的所有数据的确认。若确认号=n,则表明到序号n-1为止的所有数据都已正确接收,期望接收序号为n的数据。
数据偏移:占4比特,并以4字节为单位。用来指出TCP报文段的数据载荷部分的起始处距离TCP报文段的起始处有多远。这个字段实际上是指出了TCP报文段的首部长度。
窗口: 占16比特,以字节为单位。指出发送本报文段的一方的接收窗。
确认标志位ACK: 取值为1时确认号字段才有效;取值为0时确认号字段无效。TCP规定,在连接建立后所有传送的TCP报文段都必须把ACK置1。
同步标志位SYN: 在TCP连接建立时用来同步序号。
终止标志位FIN: 用来释放TCP连接。
复位标志位RST: 用来复位TCP连接。
推送标志位PSH: 接收方的TCP收到该标志位为1的报文段会尽快上交应用进程,而不必等到接收缓存都填满后再向上交付。
校验和: 占16比特,检查范围包括TCP报文段的首部和数据载荷两部分。在计算校验和时,要在TCP报文段的前面加上12字节的伪首部。
紧急指针: 占16比特,以字节为单位,用来指明紧急数据的长度。
填充: 由于选项的长度可变,因此使用填充来 确保报文段首部能被4整除,(因为数据偏移字段,也就是首部长度字段,是以4字节为单位的)。
为什么需要三次握手
使TCP双方能够确知对方的存在
使TCP双方能够协商一些参数( 最大窗口值是否使用窗口扩大选项和时间戳选项,以及服务质量等)
使TCP双方能够对运输实体资源(例如缓存大小连接表中的项目等)进行分配
为什么不是两次或四次握手
1、三次握手可以验证双方的接收和发送能力;
2、三次握手可以阻止重复历史连接的初始化;
客户端连续发送多次 SYN (都是同一个四元组)建立连接的报文,在网络拥堵情况下:
一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是x+1。
客户端收到后,发现自己期望收到的确认号应该是 m+1,而不是 x+1,于是就会回 RST 报文。
服务端收到 RST 报文后,就会释放连接。
后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。
上述中的「旧 SYN 报文」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。
而如果是两次握手的话,服务端没有中间状态给客户端来阻止历史连接,收到一个客户端的SYN报文就会分配资源建立连接,此时便可以向客户端发送数据了,而此时的客户端还没有进入ESTABLISHED 状态,当客户端发现确认号错误时回复RST报文,又断开连接,导致资源浪费。
3、三次握手可以同步双方的初始序列号;
首先,序列号的作用是:接收方可以去除重复数据、接收方保证数据包按序接收。因为TCP是稳定的连接,它必须要确保双方的初始序列号能可靠的同步,而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
4、三次握手可以避免资源浪费;
5、小结:
两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
六、TCP四次挥手
图示
为什么需要四次挥手,而不是三次
TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。
这就意味着,关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了,但是客户端还能接收服务端的数据。
服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,但此时服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。
简单地说,前 2 次挥手用于关闭一个方向的数据通道,后两次挥手用于关闭另外一个方向的数据通道。
2MSL是什么,为什么要有这个延迟等待
MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。