一、网络协议
1.TCP/IP
TCP/IP (Transfer Control Protocol/Internet Protocol) 协议,我们也叫做⽹络通讯协议。包含了⼀系列构成互联⽹基础的⽹络协议,是 Internet 的核⼼协议。
国际标准化组织(ISO)制定了一个用于计算机或通信系统间互联的标准体系,一般称为 OSI 参考模型或七层模型;
而与工业生产中实际用到的 TCP/IP 五层模型相比。这两者者的关系就好比,车间里面一个贴在墙上很规范的流程图,一个是实际生产中因为成本/便捷/效率等因素最终采用的流程。
(还有一种说法,将物理层与数据链路层合并称为网络接入层,这种情况下 TCP/IP 为四层模型)
但是不管是七层模型还是五层模型,他们实现的目无非是为了解决两个问题:
- 数据转成电信号;
- 目标主机的寻址。
其中,传输层有 TCP/UDP 两种连接方式,所以对应的 Socket 也有两种不同实现方式,掌握 Socket 的前提是了解清楚这两种协议。
- 应用层:为终端应用提供的服务,如我们的浏览器交互时候需要用到的 HTTP 协议,邮件发送的 SMTP,文件传输的 FTP 等。
- 表示层:为数据提供压缩,解压缩,解释,加密等功能。
- 会话层 :通过传输层,建立了数据的传输通道。不同应用有自己不同的会话标识,所以传输过来的数据根据会话标识能够知道跟电脑的哪个应用在通信的。
- 传输层:定义了数据传输的协议和端口,比如是选择可靠的 TCP 还是不可靠的 UDP。在它上层的会话层是不关心它选择什么协议的,只要传输层能够提供传输的服务就行。传输层对于会话层来说是透明的。
- 网络层 :在网络环境中,每台设备都有一个标识叫做 IP。数据在互联网的传输中都需要告知自己和对应的 IP,所以在这一层中主要是将数据添加一些网络信息,这一层的数据叫做包。
- 数据链路层 :除了 IP 地址外,每台设备也有唯一的一个 MAC 地址。因为 IP 是需要网络设备如路由器来解析的,局域网中不需要这种设备,一般是借助 MAC 地址寻址的。就像出门在外你有一个正式的名字,但是回到家里可能大家叫你另一个小名之类。这一层的数据叫做帧。
- 物理层:一些物理设备的标准,如网卡网线,传输速率定义,最终实现将我们的数据转成电信号传输。
在 TCP/IP 协议中,TCP 协议和 IP 协议分别完成不同的任务。
TCP 是⽤来检测⽹络传输中的差错。
IP 协议可以将多个交换⽹络连接起来,在源地址和⽬的地址之间传送数据包。同时,它还提供数据重新组装功能,以适应不同⽹络对数据包⼤⼩的要求。
在 TCP/IP 协议中,使⽤ IP 协议传输的数据包就是 IP 数据包。 IP 报⽂是在⽹络层传输的数据单元,也叫 IP 数据报,IP数据报由首部与数据部分组成。
- 在一个发送流程中:
应⽤层放⼊数据 ------> 传输层信息插⼊ TCP ⾸部 ------> ⽹络层信息插⼊ IP ⾸部 ------>数据链路层插⼊以太⽹头 ------> 物理层通过⽹卡发送 - 在一个接收流程中:
物理层⽹卡接收 ------>数据链路层解析以太⽹头 ------>⽹络层解析 IP ⾸部 ------>传输层解析 TCP ⾸部 ------> 应⽤层获得数据
(本部分只做了解)在一段 IP数据报中的详细数据
- 版本 : IP 协议的版本,⽬前的 IP 协议版本号为 4,下⼀代 IP 协议版本号为 6。
- ⾸部⻓度:IP 报头的⻓度。固定部分的⻓度(20 字节)和可变部分的⻓度之和。共占 4 位。 最⼤为 1111,即 10 进制 的 15,代表 IP 报头的最⼤⻓度可以为 15 个 32bits(4 字节),也 就是最⻓可为 15*4=60 字节,除去固定部分的⻓度 20 字节,可变部分的⻓度最⼤为 40 字 节。
- 服务类型:Type Of Service。
- 总⻓度:IP 报⽂的总⻓度。报头的⻓度和数据部分的⻓度之和。
- 标识:唯⼀的标识主机发送的每⼀份数据报。通常每发送⼀个报⽂,它的值加⼀。当 IP 报⽂⻓ 度超过传输⽹络的 MTU(最⼤传输单元)时必须分⽚,这个标识字段的值被复制到所有数据分⽚的标识字段中,使得这些分⽚在达到最终⽬的地时可以依照标识字段的内容重新组成原先的数据。
- 标志:共 3 位。R、DF、MF 三位。⽬前只有后两位有效,DF 位:为 1 表示不分⽚,为 0 表 示分⽚。MF:为 1 表示 "更多的⽚",为 0 表示这是最后⼀⽚。
- ⽚位移:本分⽚在原先数据报⽂中相对⾸位的偏移位。(需要再乘以 8)
- ⽣存时间:IP 报⽂所允许通过的路由器的最⼤数量。每经过⼀个路由器,TTL 减 1,当为 0 时,路由器将该数据报丢弃。TTL 字段是由发送端初始设置⼀个 8 bit 字段. 推荐的初始值由分 配数字 RFC 指定,当前值为 64。发送 ICMP 回显应答时经常把 TTL 设为最⼤值 255。
- 协议:指出 IP 报⽂携带的数据使⽤的是那种协议,以便⽬的主机的 IP 层能知道要将数据报上 交到哪个进程(不同的协议有专⻔不同的进程处理)。和端⼝号类似,此处采⽤协议号,TCP 的协议号为 6,UDP 的协议号为 17。ICMP 的协议号为 1,IGMP 的协议号为 2.
- ⾸部校验和:计算 IP 头部的校验和,检查 IP 报头的完整性。
- 源 IP 地址:标识 IP 数据报的源端设备。
- ⽬的 IP 地址:标识 IP 数据报的⽬的地址。
2. TCP 协议之三次握手和四次挥手
TCP协议是面向连接,且具备顺序控制和重发机制的可靠传输。他的可靠性是在于传输数据前要先建立连接,确保要传输的对方有响应才进行数据的传输。因此 TCP 有个经典的 3 次握手和 4 次挥手。
(1)三次握手
握手的目的是为了相互确认通信双方的状态都是正常的,没有问题后才会进行正式的通信:
- 第一次握手:客户端发送请求连接的消息给服务端,但发出去的消息是否到达并不清楚,要基于第二次握手的反馈。 在第一次握手中,由客户端发送请求连接即 SYN = 1,TCP 规定 SYN=1 的时候,不能够携带数据。 但是需要消耗⼀个 seq 序号。因此,产⽣了⼀个序号 seq = x。
- 第二次握手:服务端返回消息说明客户端的消息收到了,此时它也纠结了,我的反馈信息对方有没有收到,所以得依托第三次得握手。 在第二次握手中,服务端收到了客户端发送的消息,向客户端发送确认。发送 SYN = 1,表示请求连接已经收到,然后发送确认 ACK=1,把 TCP 包中 ACK 位设置为 1。再来发送⼀个新的序列号 seq =y,确认好 ack = x + 1。
- 第三次握手:客户端反馈第二次握手的消息收到了。至此,通信双发的发送消息和接受消息能力都得到了检验。 但是客户端收到服务端的确认之后, 还需要向服务端给出确认,说明自己收到确认包了。 设置确认 ACK = 1,ack = y + 1。而顺序号 seq = x + 1。至此双⽅建⽴稳定的连接,此时 ACK 报⽂可以携带数据。
3 次握手的整个过程看着似乎有点过于谨慎,但是互联网的初期网络基础设施是很落后的,丢包的概率非常大的。而且这个过程也只是在通信前期建立连接的时候进行,3 次握手过后就是正常的消息传输了。
(2)四次挥手
4 次挥手的目的跟 3 次握手目的是一样的,确保双方消息状态的准确:
- 第一次挥手:客户端(服务端也可以主动断开)向服务端说明想要关闭连接; 客户端打算关闭连接,此时会发送⼀个 TCP ⾸部 FIN 标志位被置为 1 的报⽂,也即 FIN 报⽂,之后客户端进⼊ FIN_WAIT_1 状态。
- 第二次挥手:服务端首先回复第一次的消息已经收到。但是并不是立马关闭,因为此时服务端可能还有数据在传输中。服务端收到报⽂后,就向客户端发送 ACK 应答报⽂,接着服务端进⼊ CLOSED_WAIT 状态。
- 第三次挥手:客户端收到服务端的 ACK 应答报⽂后,之后进⼊ FIN_WAIT_2 状态。但此时服务端可能还有⼀些数据未处理完。等待服务端处理完数据后,也向客户端发送 FIN 报⽂,告知一切都准备好了,我要断开连接了,之后服务端进⼊ LAST_ACK 状态。
- 第四次挥手: 客户端收到服务端的 FIN 报⽂后,回⼀个 ACK 应答报⽂,之后进⼊ TIME_WAIT 状态。服务端收到了 ACK 应答报⽂后,就直接进⼊了 CLOSE 状态,⾄此服务端已经完成连接的关闭。客户端在经过 2MSL ⼀段时间后,也就⾃动进⼊了 CLOSE 状态,⾄此客户端也完成连接的关闭。2MSL为⼀个发送和⼀个回复所需的最⼤时间。
3.UDP协议
UDP 是一种不可靠的传输机制,但是它的数据报文比 TCP 小,所以相同数据的传输 UDP 所需的带宽更少,传输速度更快。它不要事先建立连接,知道对方的地址后直接数据包就扔过去,也不保证对方有没有收到。
UDP 数据报主要由两个部分组成:⾸部 + 数据部分。
⾸部部分很简单,只有 8 个字节,由四个字段组成,每个字段的⻓度都是两个字节。
字段含义 :
- 源端⼝: 源端⼝号,需要对⽅回信时选⽤,不需要时全部置 0。
- ⽬的端⼝:⽬的端⼝号,在终点交付报⽂的时候需要⽤到。
- ⻓度:UDP 的数据报的⻓度(包括⾸部和数据)其最⼩值为 8(即只有⾸部)
- 校验和:检测 UDP 数据报在传输中是否有错,有错则丢弃。
特点:
- tcp协议是⾯向连接、可靠、字节流
- udp 协议是⽆连接、不可靠、数据报⽂字段
性能:
- tcp 协议传输效率慢,所需要资源多
- udp 协议传输效率快,所需要资源少
使用场景:
- tcp 协议常⽤于⽂件,邮件传输
- udp 协议常⽤于语⾳,视频,直播等实时性要求较⾼的场所
二、socket网络编程(UDP篇)
TCP/IP 五层⽹络模型的
应⽤层编程接⼝
称为 Socket API,Socket(套接字) 本身有 "插座" 的意 思,它是对⽹络中不同主机上的应⽤进程之间进⾏双向通信的端点的抽象。 ⼀个套接字就是⽹络上进程通信的⼀端,提供了应⽤层进程利⽤⽹络协议交换数据的机制。从所处的地位来讲,套接字上联应⽤进程,下联⽹络协议栈,是应⽤程序通过⽹络协议进⾏通信的接⼝。Socket 套接字类型
- 流式套接字 (SOCKET_STREAM) 提供了⼀个⾯向连接、可靠的数据传输服务,数据⽆差错、⽆重复的发送且 按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收⽅。数据被看作是字节流,⽆⻓度限制。
- 数据报套接字 (SOCK_DGRAM) 提供⽆连接服务。数据包以独⽴数据包的形式被发送,不提供⽆差错保证, 数据可能丢失或重复,顺序发送,可能乱序接收。
- 原始套接字 (SOCK_RAW) 可以对较低层次协议如 IP、ICMP 直接访问。
因为在网络上传输的数据都是以字节流的形式进行传输,所以在正式传输之前,我们需要将IP字符串转换为网络字节串,这样子才能告诉协议们建立连接时双方的IP地址,确保找到正确的传输目标。
1.IP字符串与网络字节序的转换
(1)IP 字符串转换为网络字节序
方法1:调用inet_addr函数
c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef unsigned int uint32_t;
typedef unsigned int in_addr_t;
in_addr_t inet_addr(const char *cp);
功能:将cp指向的IP字符串转成⽹络字节序
返回值: 成功返回⽹络字节序,失败返回INADDR_NONE [0xffffffff]
注意:它不能识别255.255.255.255
方法2:调用inet_aton函数
c
int inet_aton(const char *cp, struct in_addr *inp);
功能:将cp指向的IP字符串转成⽹络字节序并保存到inp的地址中。
参数:
@cp IP字符串⾸地址
@inp 存放⽹络字节序的地址
返回值: 成功返回⾮0,失败返回0
struct in_addr
{
unsigned int s_addr;
};
实质:将网络字节序存储在inp结构体指针中的 s_addr这个变量中。
(2)网络字节序转换为 IP 字符串
方法:调用inet_ntoa函数
c
char *inet_ntoa(struct in_addr in);
功能:将IP⽹络字节序转换成IP字符串
参数:
@in IP⽹络字节序
返回值: 成功返回IP字符串⾸地址,失败返回NULL
(3)主机字节序转换为网络字节序
主机字节序是什么?
我们知道,计算机数据存放到内存中有两种存储方式:
- 大端存储:数据高字节序的字节位 存放到 低地址处
低字节序的字节位 存放到 高地址处 - 小端存储:数据高字节序的字节位 存放到 高地址处
低字节序的字节位 存放到 低地址处
因为存储方式的不同,我们将使用小端存储方式的网络字节序称为主机字节序。
而网络字节序采用大端存储形式。
c
short htons(short data);
功能:将short类型的整数从主机字节序转成⽹络字节序
参数:
@data 序号转换的整数
返回值:得到的⽹络字节序
判断大小端存储形式的方法:
以int类型的1为例,1在内存中存储的大小端格式如下:
如果我们可以得到1在内存中存储的第一个字节,那么我们就可以知道当前系统是大端存储还是小端存储了。
- 如果第一个字节为1,就是小端字节序;
- 如果第一个字节为0,就是大端字节序。
程序如下:
c
#include <stdio.h>
int main()
{
int a = 1;
char pc = *(char*)(&a);
if (pc == 1)
printf("第一个字节为1,小端存储\n");
else
printf("第一个字节为0,大端存储\n");
return 0;
}
(4)网络字节序转换为十进制数
c
int atoi(const char *nptr);
功能:把ntpr 所指向的整数字符串转换成整数。
参数:
@ nptr 字符串
返回值:成功,返回转换后的整数
失败,返回0
注意:若是只有+,-和整数字符则能正常转换,其他字符返回0
-------------------------------------------------------------------------
uint32_t ntohs(uint32_t netlong); [network to host short]
功能:把⽹络字节序转换为主机端⼝
参数:
@ netlong ⽹络字节序
返回值: 返回对应的主机端⼝
2.UDP编程
UDP 是⼀个传输层的⽆连接的协议,我们编写代码⼀般是分为两个端。⼀个我们称之为发送端,另⼀ 个我们称之为接收端。正常⼀般是接收端先运⾏,然后等待结束发送端发送过来的数据。
(1)UDP发送端
编写UDP发送端分为两个步骤
1.创建套接字(实质类似于对⽂件的操作)
创建套接字需调用socket函数
c
int socket(int domain, int type, int protocol);
参数:
@domain 地址族
AF_UNIX 本地unix域通信
AF_INET IPV4 ineter⽹通信 [我们使⽤这个]
@type 使⽤协议类型
SOCK_STREAM 流式套接字(TCP)
SOCK_DGRAM 报⽂套接字(UDP)
SOCK_RAW 原始套接字: (IP,ICMP)
@protocol 协议编号
0 : 让系统⾃动识别
返回值: 成功返回得到的⽂件描述符。
失败返回 -1
-------------------------------------------------------------------------
示例用法:
int fd = socket(AF_INET,SOCK_DGRAM,0);
2.发送数据
发送数据需调用sendto函数
c
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
@sockfd 套接字
@buf 数据存放的⾸地址
@len 期望发送的数据⼤⼩
@flags 操作⽅式 (0 表示默认操作)
@dest_addr 向指定的地址发送数据
@addrlen 发送的地址的⼤⼩
返回值:
成功返回实际发送的字节数,失败返回-1
-------------------------------------------------------------------------
对于 struct sockaddr *dest_addr
Linux操作系统内置了两种结构体可供选择,用来处理网络通信的地址:
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
//sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了
};
//该结构体解决了sockaddr的缺陷,把 port和addr 分开储存在两个变量中
struct sockaddr_in {
short int sin_family; //地址族 (Address Family)
//AF_INET 表示IPv4网络协议
//AF_INET6 表示IPv6
unsigned short int sin_port; //16位的端口号
struct in_addr sin_addr; //32位的IP地址
unsigned char sin_zero[8]; //暂时没什么卵用
};
struct in_addr {
uint32_t s_addr; //32位的IPv4地址
};
创建一个UDP通信发送方的全部代码如下:
c
//创建UDP通信
//UDP发送方
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
void send_data(int sockfd, struct sockaddr_in *addr, int len)
{
int n = 0;
char buf[1024] = {0}; //数据区
while(1){
putchar('>');
memset(buf, 0, sizeof(buf));
//用户输入
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = '\0'; // 将 \n ---> \0
//发送
n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)addr, len);
if(n < 0){
perror("Fail to sendto");
exit(EXIT_FAILURE);
}
if(strncmp(buf, "quit", 4) == 0){
break;
}
}
}
//运行形式: ./a.out ip port
int main(int argc, const char *argv[])
{
if(argc != 3){
fprintf(stderr, "Usage : %s ip port!\n", argv[0]);
exit(EXIT_FAILURE);
}
//1.通过socket创建文件描述符
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0){
perror("Fail to socket!");
exit(EXIT_FAILURE);
}
//2.填充服务器的ip port
struct sockaddr_in peer_addr;
memset(&peer_addr, 0, sizeof(peer_addr)); //凊空杂数据
peer_addr.sin_family = AF_INET; //指定协议族
peer_addr.sin_port = htons(atoi(argv[2])); //端口号
peer_addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址
//3.发送数据
int len = sizeof(peer_addr);
send_data(sockfd, &peer_addr, len);
//4.关闭文件描述符
close(sockfd);
return 0;
}
(2)UDP接收端
编写UDP发送端分为两个步骤
1. 把 ip 和端口与当前进程绑定
调用bind函数
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
功能:把ip地址和端⼝绑定到socket中去。
参数:
@sockfd socket创建的⽂件描述符
@addr 把IP和地址设置到对应的结构体中去。
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr
{
unsigned int s_addr;
}
@addrlen 表示 addr 参数对应类型的地址信息结构体的⼤⼩
返回值: 成功 返回0;失败返回 -1 ,并设置errno
示例用法:
c
1)定义结构体
struct sockaddr_in my_addr;
memest(&my_addr,0,sizeof(my_addr));
2)填充数据
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(atoi(argv[2]));
my_addr.sin_addr.s_addr = inet_addr(argv[1]);
3)绑定数据
if(bind(sockfd,(struct sockaddr *)&my_addr),sizeof(my_addr) < 0)
{
...
}
2. 接收数据
调用recvfrom函数
c
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen)
参数:
@sockfd 套接字
@buf 数据存放的⾸地址
@len 期望接收的数据⼤⼩
@flags 操作⽅式 0 表示默认操作
@src_addr 获得发送⽅地址,谁发送的获得谁的地址。
@addrlen 值结果参数,必须进⾏初始化, 表示表示对⽅实际地址的⼤⼩。
返回值:
成功返回实际接收的字节数,失败返回-1
示例用法:
c
struct sockaddr_in peer_addr;
socklen_t addrlen = sizeof(struct sockaddr_in);
n = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&peer_addr,&addrlen);
创建一个UDP接收方的全部代码如下:
c
//创建UDP通信
//UDP接收方
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
void recv_data(int sockfd)
{
int n = 0;
char buf[1024] = {0};
struct sockaddr_in client_addr;
int len = sizeof(client_addr);
while(1){
puts("--------------------");
memset(buf, 0, sizeof(buf));
n = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &len);
if(n < 0){
perror("Fail to recvfrom");
exit(EXIT_FAILURE);
}
printf("Recv from IP = %s\n", inet_ntoa(client_addr.sin_addr));
printf("Recv from Port = %d\n", ntohs(client_addr.sin_port));
printf("Recv %d bytes : <%s>\n", n, buf);
if(strncmp(buf, "quit", 4) == 0){
break;
}
}
}
int main(int argc, const char *argv[])
{
if(argc != 3){
fprintf(stderr, "Usage : %s ip port!\n", argv[0]);
exit(EXIT_FAILURE);
}
//1.通过socket创建文件描述符
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0){
perror("Fail to socket!");
exit(EXIT_FAILURE);
}
//2.填充服务器自己的ip port
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET; //协议簇
my_addr.sin_port = htons(atoi(argv[2])); //端口号
my_addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址
//3.把ip port与socket绑定
if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0){
perror("Fail to bind");
exit(EXIT_FAILURE);
}
printf("wait recv data...\n");
//3.接收数据
recv_data(sockfd);
//4.关闭文件描述符
close(sockfd);
return 0;
}