一、OSI网络七层模型
OSI模型将整个网络通信过程分解为七个层次,每个层次都为网络通信提供了特定的功能。以下是OSI模型的七个层次,从上到下依次是:
-
应用层(Application Layer):为应用软件提供网络服务,如HTTP、FTP、SMTP等协议,处理网络应用程序的具体功能。
-
表示层(Presentation Layer):确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取,负责数据格式转换、数据加密解密等。
-
会话层(Session Layer):建立、管理和终止应用程序之间的会话,提供数据交换定界和同步功能,包括建立检查点和恢复方案。
-
传输层(Transport Layer):负责主机中两个进程的对话控制,如TCP(传输控制协议)和UDP(用户数据报协议),处理数据包的顺序、错误校验、流量控制等。
-
网络层(Network Layer):负责数据包从源到宿的传递和路由选择,如IP(互联网协议)。
-
数据链路层(Data Link Layer):传输有地址的帧以及错误检测功能,如以太网协议。
-
物理层(Physical Layer):处理通过物理设备传输数据的技术细节,如网线、光纤、无线电波等。
-
OSI模型的每一层都为它的上层提供服务,同时依赖于它的下层。这种分层的设计使得网络通信更加模块化,便于理解和设计复杂的网络系统。在实际应用中,TCP/IP模型是更为广泛使用的网络模型,它简化了层次结构,但基本原理与OSI模型相似。
后来人们对 OSI 进行了简化,合并了一些层,最终只保留了 4 层,从下到上分别是接口层、网络层、传输 层和应用层,这就是大名鼎鼎的 TCP/IP 模型。
本次讲的是spcket编程,是站在传输层的基础上,可以使用TCP/UDP协议。
二、网络数据传输过程
平常使用的软件都是通过应用层来访问网络,程序产生的数据会一层层地往下传输,直到最后的网络接口层,就通过网线发送到互联网上去了。
数据向下:原始数据每通过一层,就会多一层协议的包装,通过四层,原始数据变成包装数据。
数据向上:包装数据每通过一层,就会少一层协议的包装,通过四层,包装数据被完全拆解成原始数据。
1.TCP通信协议
TCP 是面向连接的传输协议,可靠性传输,建立连接时要经过三次握手,断开连接时要经过四次挥手,中间 传输数据时也要回复 ACK 包确认,多种机制保证了数据能够正确到达,不会丢失或出错。
1、TCP 的 3 次握手过程 1、客户端发送 TCP 连接请求 客户端会随机一个初始序列号 seq=x(client_isn),设置 SYN=1,表示这是 SYN 握手报文。然后 就 可以把这个 SYN 报文发送给服务端了,表示向服务端发起连接,之后客户端处于同步已发送状态。
2、服务端发送针对 TCP 连接请求的确认,服务端收到客户端的 SYN 报文后,也随机一个初始序列号 (server_isn)(seq=y),设置 ack=x+1, 表示收到了客户端的 x 之前的数据,希望客户端下次发送的数据从 x+1 开始。设置 SYN=1 和 ACK=1。表示这是一个 SYN 握手和 ACK 确认应答报文。 最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于同步已接收状态。
3、客户端发送确认的确认 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,将 ACK 置为 1 ,表示这是一个 应答报文 ack=y+1 ,表示收到了服务器的 y 之前的数据,希望服务器下次发送的数据从 y+1 开始。最 后把报文发送给服务端,这次报文可以携带数据,之后客户端处于连接已建立 状态。服务器收到客户 端的应答报文后,也进入连接已建立状态 通过这样的三次握手过程,TCP 能够确保双方能够收到对方的请求和回应,并且双方都知道彼此的初始 序列号和确认号。这样建立起来的连接可以提供可靠的数据传输和顺序控制。 ACK:确认序号有效。 SYN:发起一个新连接。
CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)
LISTEN:等待从任何远端 TCP 和端口的连接请求。
SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
syn_sent SYN_RCVD: 这个状态表示接受到了 SYN 报文,在正常情况下,这个状态是服务器端的 SOCKET 在建立 TCP 连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用 netstat 你是很难看到这种状态的,除非你特 意写了一个客户端测试程序,故意将三次 TCP 握手过程中最后一个 ACK 报文不予发送。因此这种状态时,当收到客户端的 ACK 报文后,它会进入到 ESTABLISHED 状态 ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态
为什么是三次握手,为什么不是两次或者四次?
主要原因:防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误 如果采用两次握手会出现以下情况: 客户端向服务器端发送的请求报文由于网络等原因滞留,未能发送到服务器端,此时连接请求报文失效, 客户端会再次向服务器端发送请求报文,之后与服务器端建立连接,当连接释放后,由于网络通畅了,第一次客 户端发送的请求报文又突然到达了服务器端,这条请求报文本该失效了,但此时服务器端误认为客户端又发送了 一次连接请求,两次握手建立好连接,此时客户端忽略服务器端发来的确认,也不发送数据,造成不必要的错误 和网络资源的浪费。 如果采用三次握手的话,就算那条失效的报文发送到服务器端,服务器端确认并向客户端发送报文,但此时 客户端不会发出确认,由于客户端没有确认,由于服务器端没有接收到确认,就会知道客户端没有请求连接。
为什么不是四次?如果三次就能够确定正常连接,就没有必要在进行确认,来浪费资源了
2.UDP通信协议
UDP 是非面向连接的传输协议,没有建立连接和断开连接的过程,它只是简单地把数据丢到网络中,也不 需要 ACK 包确认。在数据传输过程中延迟小、数据传输效率高。 当强调传输性能而不是传输的完整性时,如:音频和多媒体应用,UDP 是最好的选择。
总结:
短链接,不连接通信
相对来说没有 TCP 那么稳定
有可能丢失相应数据
它的发送速度相对TCP来说比较快
3.网络编程的 IP 地址
windows 下:ipconfig
Linux 下:ifconfig
1、ipv4地址是32(bit)地址数据、现在发展到ipv6
2、ip由网络号和主机号组成
3、子网掩码:255.255.255.0
4、端口
端口(Port)是一个逻辑概念,用于区分不同的服务或进程。端口号与IP地址一起工作,帮助网络协议(如TCP/IP)确定数据应该被送往网络中哪一台主机的哪个具体服务。端口号是16位的数字,范围从0到65535。
端口号分为几个范围:
-
知名端口(Well-Known Ports):范围从0到1023,通常被系统或者常见服务和应用程序使用。例如,HTTP服务通常使用端口80,HTTPS服务使用端口443,而FTP服务使用端口21。
-
注册端口(Registered Ports):范围从1024到49151,用于用户或应用程序。这些端口号用于运行用户开发的服务或应用程序,但它们不像知名端口那样广为人知。
-
动态或私有端口(Dynamic or Private Ports):范围从49152到65535,通常不由任何服务固定使用,而是由各种应用程序在需要时临时使用。
端口的作用包括:
-
区分服务:不同的网络服务监听不同的端口,例如,Web服务器监听端口80或443,而电子邮件服务器可能监听端口25(SMTP)或110(POP3)。
-
进程间通信:在一台主机上,不同的进程可以通过不同的端口号进行网络通信。
-
网络配置:端口号也用于配置网络服务,如防火墙和路由器规则,允许或阻止对特定端口的访问。
-
安全性:通过限制对某些端口的访问,可以提高网络安全性。例如,关闭不常用或不必要的端口可以减少潜在的安全风险。
在网络编程中,端口的使用通常涉及以下步骤:
-
绑定(Binding):一个网络应用程序在启动时会绑定到一个特定的端口号,这样它就可以监听进入该端口的网络请求。
-
监听(Listening):绑定到端口的应用程序会进入监听状态,等待网络请求的到来。
-
连接(Connecting):当客户端程序需要与服务端通信时,它会指定服务器的IP地址和端口号来建立连接。
-
通信:一旦连接建立,客户端和服务器就可以通过这个端口进行数据传输。
端口的使用对于网络通信至关重要,它们使得复杂的网络环境能够有序地运行。
5.TCP编程协议框架
6.TCP 服务端编写流程
1、创建套接字:sockeet
int socket(int domain, int type, int protocol);
参数: domain: 网域
AF_INET :IPv4
AF_INET6 :IPv6
AF_UNIX :本地通讯
type:选择传输协议 tcp /udp
SOCK_STREAM ;tcp
SOCK_DGRAM : udp
protocol: 基本废弃,一般赋 0
返回值:
成功返回描述网络套接字 sockfd,失败返回-1
举例:
创建描述网络的套接字:
int sfd = socket(AF_INET,SOCK_STREAM,0)
2、绑定:bind
绑定一个端口号和 IP 地址,使套接口与指定的端口号和 IP 地址相关
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen)
参数:sockfd 为前面 socket 的返回值,描述网络的套接字
my_addr:封装 ip 地址和端口号 struct sockaddr //一般很少用
struct sockaddr //一般很少用
{unsigned short int sa_family; // AF_INET。char sa_data[14]; //IP 和端口
};
#include<netinet/in.h>
struct sockaddr_in //常用的结构体
{unsigned short int sin_family; //AF_INETuint16_t sin_port; //使用的 port 编号struct in_addr sin_addr; // IP 地址unsigned char sin_zero[8]; //未使用
};端口:10000-65535
12345
htons h-host to n-net s:short
转化:uint32_t htonl(uint32_t hostlong);//本函数将一个 32 位数从主机字节顺序转换成 无符号长整型网络字节
顺序:
uint16_t htons(uint16_t hostshort);//将一个无符号短整型的主机数值转换为网络字节顺序
uint32_t ntohl(uint32_t netlong);//将一个无符号长整形数从网络字节顺序转换为主机字节顺序。
uint16_t ntohs(uint16_t netshort);//将一个 16 位数由网络字节顺序转换为主机字节顺序。
IP 的填写方式:
struct in_addr
{uint32_t s_addr; //=inet_addr("192.168.1.22");
};
addrlen:第二个参数大小
返回值:成功则返回 0,失败返回-1
举例:
#define PORT 33333
#define IP "192.168.110.123" struct sockaddr_in ser_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(PORT);
ser_addr.sin_addr.s_addr = inet_addr(IP);
inet_aton(“192.168.1.22”,&ser_addr.sin_addr);
bind(sfd,(struct sockaddr)&ser_addr,sizeof(ser_addr));
3、监听:listen
设置允许的最大连接数(瞬时处理的阈值),listen 函数
使服务器的这个端口和 IP 处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求, 端口就会接受这个连接
int listen(int sockfd, int backlog);
参数sockfd 为前面 socket 的返回值,sfdbacklog 指定同时能处理的最大连接要求,通常为 10 或者 5。 最大值可设至 128(不是最多可以连接 128 个客户端,是一个瞬时处理的阈值)返回值:成功则返回 0,失败返回-1
例子:listen(sfd,10);
4、等待客户端连接:accept
int accept(int sockfd, struct sockaddr *addr,socklen_t *addrlen);
参数 :
sockfd 为前面 socket 的返回值,即 sfd
addr:提供空间,用于接受客户端的 ip 地址和端口号
addrlen:第二个参数大小
返回值: 返回新的套接字描述符,专门用于与建立的客户端通信 失败-1;
举例:
struct sockaddr_in cli_addr = {0};
socklen_t len = sizeof(cli_addr);
int cfd = accept(sfd,(struct sockaddr *)&cli_addr,&len)
5、write 和 read 进行通信
ssize_t send(int s, const void *buf, size_t len, int flags);
参数: s:通信套接字
buf:要发送的数据缓冲区
len: 数据长度
flags:一般赋 0 .阻塞
返回值:成功返回真正发送的数据长度,失败-1
ssize_t recv(int s, void* buf,size_t len,int flags);
参数 s:通信的套接字
buf:存放接收数据的缓冲区
len: 数据长度
flags:一般赋 0 .阻塞
返回值:成功返回真正接收的数据长度,失败-1
7.TCP 客户端编写流程
1、创建套接字:socket
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
domain: 网域
AF_INET :IPv4
AF_INET6 :IPv6
type:选择传输协议 tcp /udp
SOCK_STREAM ;tcp
SOCK_DGRAM : udp
protocol: 基本废弃,一般赋 0
返回值:成功返回套接字 fd,失败返回-1
2、connect : 连接
int connect(int sockfd, const struct sockaddr*serv_addr, socklen_t addrlen)
参数:sockfd 为前面 socket 的返回值,即 fd
serv_addr 为结构体指针变量,存储着远程服务器的 IP 与端口号信息。
addrlen 表示结构体变量的长度
返回值:成功则返回 0,失败返回-1
举例:
int fd = socket(AF_INET,SOCK_STREAM,0);
#define PORT 33333
#define IP "192.168.110.123" struct sockaddr_in ser_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(PORT);
ser_addr.sin_addr.s_addr = inet_addr(IP);
connect(fd,(struct sockaddr *)&ser_addr,sizeof(ser_addr))
3、write 和 read 进行通信 或者 send 和 recv 进行通信
TCP 问题: 当服务器结束之后,再次运行会出现 bind 错误(地址被占用)
解决办法:
int sfd = socket(AF_INET,SOCK_STREAM,0); 创建套接字之后,
用:
int val = 1;
setsockopt(sfd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof(val)); setsockopt(sfd,SOL_SOCKET,SO_REUSEPORT,&val,sizeof(val));
7、单播、广播、多播
单播 -- 一对一 广播 -- 一对多 组播 -- 多
UDP 不像 TCP,无需在连接状态下交换数据,因此基于 UDP 的接收方和发送方也无需经过连接过程。也就 是说,不必调用 listen()和 accept()函数。UDP 中只有创建套接字的过程和数据交换的过程。不管是服务器端还是 客户端都只需要 1 个套接字
udp 具有广播功能,即一个发送方,多个接收方; 广播:处于局部网中的所有设备都可以接收到消息 比如:校园广播 广播的地址:网络号不变,主机号为 25
对一组特定的主机发送消息 比如:直播,多播(D 类),IP 地址分为:224.0.0.0~239.255.255.255