我是靠谱客的博主 发嗲鸭子,最近开发中收集的这篇文章主要介绍网络编程 - OSI和TCP/IP协议模型及TCP、UDP协议一、OSI七层协议模型及TCP/IP协议模型二、TCP协议 - TCP协议头部三、TCP协议 - 三次握手四、TCP协议 - 滑动窗口五、TCP协议 - 四次挥手六、TCP协议 - 分包和粘包七、UDP通信协议,觉得挺不错的,现在分享给大家,希望可以做个参考。
概述
网络编程 - OSI和TCP/IP协议模型及TCP、UDP协议
- 一、OSI七层协议模型及TCP/IP协议模型
- 1. OSI七层协议模型
- 2. TCP/IP四层协议模型
- 3. 协议封装
- 二、TCP协议 - TCP协议头部
- 三、TCP协议 - 三次握手
- 四、TCP协议 - 滑动窗口
- 1. 正常情况
- 2. 丢包情况
- 五、TCP协议 - 四次挥手
- 六、TCP协议 - 分包和粘包
- 1. TCP分包
- 2. TCP粘包
- 3. 分包和粘包的解决方案
- 4. 分包和粘包的解决方案示例代码
- 七、UDP通信协议
- 1. 概述
- 2. TCP和UDP的区别
- 3. UPD常用函数
- 4. 使用示例
一、OSI七层协议模型及TCP/IP协议模型
1. OSI七层协议模型
层级 | 作用 |
---|---|
应用层 | 为应用数据提供服务 |
表示层 | 数据格式化,数据加密 |
会话层 | 建立、维护和管理会话 |
传输层 | 建立、维护和管理端到端的连接,控制数据传输的方式 |
网络层 | 数据传输线路选择,IP地址及路由选择 |
数据链路层 | 物理通路的发送和数据包的划分,附加Mac地址到数据包 |
物理层 | 01比特流的转换 |
2. TCP/IP四层协议模型
层级 | 作用 |
---|---|
应用层 | 负责处理特定的应用程序细节,如ftp、smtp、ssh等 |
运输层 | 主要为两台主机上的应用提供端到端的通信,如TCP、UDP |
网络层(互联网层) | 处理分组在网络中的活动,如分组的选路 |
链路层(数据链路层/网络接口层) | 包括操作系统中的设备驱动程序、计算机中对应的 网络接口卡,01比特流的转换 |
3. 协议封装
下层协议通过封装为上层协议提供服务。应用程序数据在发送到物理网络上之前,将沿着协议栈从上往下依次传递。每层协议都将在上层数据的基础上加上自己的头部信息(有时也包括尾部信息),以实现该层的功能。
二、TCP协议 - TCP协议头部
字段名 | 说明 |
---|---|
源端口号和目的端口号 | 发送端的端口号和目的端的端口号。和IP首部的源IP地址和目的IP地址可以唯一确定一个TCP连接 |
数据序号 | 表示在这个报文段中的第一个数据字节序号 |
确认序号 | 仅当ACK标志为1时有效。确认号表示期望收到的下一个字节的序号(这个下面再详细分析) |
偏移 | 就是头部长度,有4位,跟IP头部一样,以4字节为单位。最大是60个字节 |
保留位 | 6位,必须为0 |
标志位 | 见表:TCP/IP 标志位 |
窗口大小 | 16位,代表的是窗口的字节容量,也就是TCP的标准窗口最大为2^16 - 1 = 65535个字节 |
校验和 | 源机器基于数据内容计算一个数值,收信息机要与源机器数值结果完全一样,从而证明数据的有效性。检验和覆盖了整个的TCP报文段:这是一个强制性的字段,一定是由发送端计算和存储,并由接收端进行验证的 |
紧急指针 | 是一个正偏移量,与序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式 |
选项与填充(必须为4字节整数倍,不够补0) | 最常见的可选字段的最长报文大小MSS(Maximum Segment Size),每个连接方通常都在一个报文段中指明这个选项。它指明本端所能接收的最大长度的报文段。该选项如果不设置,默认为536(20+20+536=576字节的IP数据报) |
标志位名称 | 说明 |
---|---|
URG | 紧急指针有效 |
ACK | 确认序号有效 |
PSH | 接收方应尽快将这个报文交给应用层 |
RST | 连接重置 |
SYN | 同步序号用来发起一个连接 |
FIN | 终止一个连接 |
三、TCP协议 - 三次握手
- 首先客户端向服务器端发送一段TCP报文,其中:标记位为SYN,表示“请求建立新连接”,序号为seq=x(x一般为1),随后客户端进入SYN-SENT阶段。
- 服务器端接收到来自客户端的TCP报文后,结束LISTEN阶段。并返回一段TCP报文,其中:标志位为SYN和ACK,表示"确认客户端的报文seq序号有效,服务器能正常接收客户端发送的数据,并同意创建新连接"(即告诉客户端,服务器收到了你的数据);序号为seq=y,确认号为ack=x+1,表示收到客户端的序号seq并将其值加1作为自己确认号ack的值;随后服务器端进入SYN-RCVD阶段。
- 客户端接收到来自服务器端的确认收到数据的TCP报文之后,明确了从客户端到服务器的数据传输是正常的,结束SYN-SENT阶段。并返回最后一段TCP报文。其中:标志位为ACK,表示“确认收到服务器端同意连接的信号”(即告诉服务器,我知道你收到我发的数据了);序号为seq=x+1,表示收到服务器端的确认号ack,并将其值作为自己的序号值;确认号为ack=y+1,表示收到服务器端序号seq,并将其值加1作为自己的确认号ack的值;随后客户端进入ESTABLISHED阶段。服务器收到来自客户端的“确认收到服务器数据”的TCP报文之后,明确了从服务器到客户端的数据传输是正常的。结束SYN-SENT阶段,进入ESTABLISHED阶段。
- 在客户端与服务器端传输的TCP报文中,双方的确认号ack和序号seq的值,都是在彼此ack和seq值的基础上进行计算的,这样做保证了TCP报文传输的连贯性。一旦出现某一方发出的TCP报文丢失,便无法继续"握手",以此确保了"三次握手"的顺利完成。
- 使用Wireshark抓包工具查看TCP三次握手过程
- 在cmd下输入:nslookup smtp.163.com 获取smtp邮件服务的IP地址
- 回到Wireshark,在使用这个过滤器后的编辑框内输入:ip.addr == 220.181.12.16
- 选择WLAN
- 点击左上角开始捕获分组按钮开始捕获
- 在cmd下输入:telnet 220.181.12.16 25
220.181.12.16 - smtp邮件服务的IP地址
25 - 端口号
捕获结果如下:
四、TCP协议 - 滑动窗口
维互发送方/接收方缓冲区,缓冲区是用来解决网络之间数据不可靠的问题,例如丢包,重复包,出错,乱序。在TCP协议中,发送方和接受方通过各自维护自己的缓冲区。通过商定包的重传机制等一系列操作,来解决不可靠的问题。
为保证数据包依次序传输,使用发送<=>确认机制
为解决发送<=>确认机制带来的效率上的弊端(数据包在网络上传输需要时间),采用了如下方案:一次发送多个包,同时确认多个
那么一次发送多少个包过去呢?一次发送多少包是最优解呢?
我们能不能把第一个和第二个包发过去后,收到第一个确认包就把第三个包发过去呢?而不是去等到第二个包的确认包才去发第三个包。这样就很自然的产生了我们"滑动窗口"的实现。
1. 正常情况
- 在上图中,我们可看出灰色1号2号3号包已经发送完毕,并且已经收到ack。这些包就已经是过去式。4、5、6、7号包是黄色的,表示已经发送了。但是并没有收到对方的ack,所以也不知道接收方有没有收到。8、9、10号包是绿色的。是我们还没有发送的。这些绿色也就是我们接下来马上要发送的包。 可以看出我们的窗口正好是7格。后面的11-16还没有被读进内存。要等4号-10号包有接下来的动作后,我们的包才会继续往下发送。
- 从上图可以看到4号包对方已经被接收到,所以被涂成了灰色。“窗口”就往右移一格,这里只要保证“窗口”是7格的。 我们就把11号包读进了我们的缓存。进入了“待发送”的状态。8、9号包已经变成了黄色,表示已经发送出去了。接下来的操作就是一样的了,确认包后,窗口往后移继续将未发送的包读进缓存,把“待发送“状态的包变为“已发送”。
2. 丢包情况
- 有可能我们包发过去,对方的ack丢了。也有可能我们的包并没有发送过去。从发送方角度看就是我们没有收到ack。
- 一般情况:一直在等ack。如果一直等不到的话,我们也会把读进缓存的待发送的包也一起发过去。但是,这个时候我们的窗口已经发满了。所以并不能把12号包读进来,而是始终在等待5号包的Ack。
如果我们这个Ack始终不来怎么办呢? 采用超时重传机制解决:
发送端每发送一个报文段,就启动一个定时器并等待确认信息;接收端成功接收新数据后返回确认信息。若在定时器超时前数据未能被确认,TCP就认为报文段中的数据已丢失或损坏,需要对报文段中的数据重新组织和重传。
五、TCP协议 - 四次挥手
- 客户端发送断开TCP连接请求的报文,其中报文中包含seq序列号,是由发送端随机生成的,并且还将报文中的FIN字段置为1,表示需要断开TCP连接。(FIN=1,seq=x,x由客户端随机生成)
- 服务端会回复客户端发送的TCP断开请求报文,其包含seq序列号,是由回复端随机生成的,而且会产生ACK字段,ACK字段数值是在客户端发过来的seq序列号基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP断开请求已经得到验证。(FIN=1,ACK=x+1,seq=y,y由服务端随机生成)
- 服务端在回复完客户端的TCP断开请求后,不会马上进行TCP连接的断开,服务端会先确保断开前,所有传输到A的数据是否已经传输完毕,一旦确认传输数据完毕,就会将回复报文的FIN字段置1,并且产生随机seq序列号。(FIN=1,ACK=x+1,seq=z,z由服务端随机生成)
- 客户端收到服务端的TCP断开请求后,会回复服务端的断开请求,包含随机生成的seq字段和ACK字段,ACK字段会在服务端的TCP断开请求的seq基础上加1,从而完成服务端请求的验证回复。(FIN=1,ACK=z+1,seq=h,h为客户端随机生成)
- 至此TCP断开连接的四次挥手过程完毕。
六、TCP协议 - 分包和粘包
1. TCP分包
- 场景
发送方发送字符串"helloworld",接收方却分别接收到两个数据包字符串,分别是字符串"hello"和字符串"world".
当发送端发送数量较多的数据时,接收端读取数据时会分批到达,造成一次发送多次读取(分包)。 - 造成分包的原因
TCP是以段(Segment)为单位发送数据的,建立TCP链接后,有一个最大消息长度(MSS)。如果应用层数据包超过MSS,就会把应用层数据包拆分,分成两个段来发送。
这个时候接收端的应用层就要拼接这两个TCP包,才能正确处理数据。
相关的,路由器有一个最大传输单元(MTU),一般是1500字节,除去IP头部20字节,留给TCP的就只有MTU-20字节。所以一般TCP的MSS为MTU-20=1460字节。当应用层数据超过1460字节时,TCP会分多个数据包来发送。
2. TCP粘包
- 场景
发送方发送两个字符串,分别是"HELLO" 和 “WORLD”,接收端却只收到了一个数据包字符串"HELLOWORLD"。
发送端发送了几次数据,接收端一次性读取了所有数据,造成多次发送一次读取;这通常是网络流量优化,把多个小的数据段集满达到一定的数据量,从而减少网络链路中的传输次数。 - 造成粘包的原因
TCP为了提高网络的利用率,会使用一个叫做Nagle的算法。该算法是指发送端即使有要发送的数据,如果很少的话,会延迟发送。如果应用层给TCP传送数据很快的话,就会把两个应用层数据包"粘"在一起,TCP最后只发一个TCP数据包给接收端。
3. 分包和粘包的解决方案
发送数据前,给数据附加一定字节的长度位。
字段名 | 说明 |
---|---|
包标识 | 包头部的特殊标识,用来标识包的开始 |
数据长度 | 数据包的大小,固定长度,2、4 或者8字节 |
数据内容 | 数据内容,长度为数据头定义的长度大小 |
- 实际操作如下:
- 发送端:先发送包表示和长度,再发送数据内容
- 接收端:先解析本次数据包的大小N,再读取N个字节,这N个字节就是一个完整的数据内容
- 接收端接收具体流程如下:
4. 分包和粘包的解决方案示例代码
- server.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#define SERVER_PORT 6666
#define SERVER_IP "127.0.0.1"
#define DATA_LENGTH 4
const char* TAG = "FBEB";
int read_package(int sock, char* buf, unsigned int bufSize) {
int tag_len = strlen(TAG);
int read_len = read(sock, buf, tag_len + DATA_LENGTH);
// 对TAG进行检查
if (strncmp(buf, TAG, tag_len) != 0) {
return -1;
}
int data_len = *((int*)(buf + tag_len));
printf("data length: %dn", data_len);
// 读取数据
int count = 0; // 当前已读取数据长度
read_len = 0;
while (count < data_len) {
read_len = read(sock, buf + count, data_len - count);
if (read_len < 1) {
fprintf(stderr, "read() - failed!n");
return -1;
}
printf("readlen: %dn", read_len);
count += read_len;
}
return data_len;
}
int main(void) {
int sock = 0;
struct sockaddr_in server_addr;
// 1.创建信箱
sock = socket(AF_INET, SOCK_STREAM, 0);
// 2.清空标签,写上地址和端口号
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET; // 选择协议族 IPV4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本地所有IP地址
server_addr.sin_port = htons(SERVER_PORT); // 绑定端口号
// 3.将标签贴至收信箱
bind(sock, (struct sockaddr*)(&server_addr), sizeof(server_addr));
// 4.将信箱挂至传达室,即可收信
listen(sock, 128);
// 万事俱备,只欠信封
printf("Wait for client connection...n");
int done = 1;
while (done) {
struct sockaddr_in client;
int client_socket = 0;
char client_ip[64] = { 0 };
socklen_t client_addr_len;
client_addr_len = sizeof(client);
client_socket = accept(sock, (struct sockaddr*)(&client), &client_addr_len);
// 打印客户端IP地址和端口号
printf("cliend ip:%st port:%dn",
inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)),
ntohs(client.sin_port));
// 读取客户端发送的数据
char buf[256] = { 0 };
int len = read_package(client_socket, buf, 256);
if (len < 0) {
fprintf(stderr, "read_package() - sock[%d] error!n", client_socket);
close(client_socket);
continue;
}
buf[len] = '