文章目录
本片博客会粘贴部分代码,想要了解更多代码信息,可访问 小编的GitHub关于本篇的代码
有关 OSI七层参考模型、TCP/IP五层(四层)参考模型
为什么会有传输层?
在协议栈中,传输层位于网络层之上,传输层协议为不同主机上运行的进程提供逻辑通信,而网络层协议为不同主机提供逻辑通信。
TCP和UDP对比
- 面向字节流和面向数据报
面向数据报:数据发送的时候有最大长度限制(65535字节),必须整条发送整条接收。
面向字节流:发送和接受数据都没有长度限制,收发数据比较灵活,数据无明显边界,容易造成tcp粘包问题。
传输协议 | 特点 | 应用场景 |
---|---|---|
UDP协议,用户数据报协议(User Datagram Protocol) | 无连接,不可靠,面向数据报。 | 数据传输速率快,实时性高,常用语音乐、视频等对数据要求不是很高,但实时性要求比较高的场景。 |
TCP协议,Transmission Control Protocol 传输控制协议 | 有连接,数据可靠传输,面向字节流 | 可以保证数据的可靠传输,因此常用语对数据安全性比较高的场景,传输效率低于UDP。 |
网络通信中服务端有固定服务器地址和固定的端口,客户端在有了服务端的ip和port后就可以向服务器发起请求。
UDP协议特性
UDP没有严格意义的发送缓冲区,调用sendto函数,内核直接将数据扔给网络,原封不动发送出去;UDP有接受缓冲区,但是接受缓冲区不能保证按发送顺序接受数据,UDP缓冲区满了,再来数据就会被丢弃。
- 基于UDP的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(⽤于无盘设备启动)
DNS: 域名解析协议 - socket套接字编程
socket是一套用于网络编程的接口,同时socket也是一个数据结构。UDP的socket既可以用来读,也可以用来写,称为全双工。
UDP报头解析
UDP报头应该有什么?UDP作为传输层协议报头,为了实现进程到进程的通信,必须带有源端口、目的端口;由于面向数据报的特点,UDP报头定长8字节,为了准确区分报头部分和数据部分,必须得有UDP长度,UDP提供非常有限的差错检测功能,报头16位UDP检验和来实现。
1、UDP长度=UDP头(8字节)+UDP数据长度
2、UDP是传输层协议,传输层负责端到端之间的数据传输,UDP头部中必须有源端口、目的端口,用来存储数据从哪个进程来到哪个进程去。
3、UDP协议首部中有⼀个16位来表示一条UDP数据的最大长度是2^16字节=65536字节=64K=8字节UDP首部+UDP数据长度,也就是说我们可以传输的有用信息不足64K,如果我们需要传输的数据达到64K, 就需要在应用层手动分包, 多次发送, 并在接收端手动拼接。
4、UDP协议的无连接:知道对端的IP的端口号就可以直接进行通信,不需要进行连接。
5、UDP协议的不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对端,, UDP协议层也不会给应用层返回任何错误信息。
6、面向数据报:应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并。
UDP网络编程过程解析
1、创建套接字
建立与网卡的关联,协议版本的选择,传输层协议的选择
int socket(int domain, int type, int protocol);
- domain: 地址域
AF_INET(协议类型)一般使用ipv4协议 - type: 套接字类型
SOCK_STREAM 流式套接字
SOCK_DGRAM 数据报套接字 - protocol(协议类型):0-默认;流式套接字默认TCP协议IPPROTO_TCP,数据报套接字默认UDP协议IPPROTO_UDP
- 返回值:套接字描述符,失败:-1
2、为套接字绑定地址信息(ip、port)
bind函数既可以绑定IPV4版本协议,也可以绑定IPV6版本,两个版本的IP头结点的大小不同,定义sockaddr为了寻求接口的统一,都要使用struct sockaddr
struct sockaddr_in //IPV4
struct sockaddr_in6 //IPV6
为socket绑定地址信息,确定socket能够操作缓冲区中的哪些数据
int bind(int sockfd, struct sockaddr *addr,socklen_t addrlen);
- sockfd: 套接字描述符
- addr: 要绑定的地址信息
- addrlen:地址信息的长度
3、 接受数据
ssize_t recvfrom(int sockfd, void *buf, size_t len,
int flags,struct sockaddr *src_addr, socklen_t *addrlen);
- sockfd: socket描述符
- buf: 用于将存储接收的数据
- len: 想要接收的数据长度
- flags: 0-默认是说如果缓冲区没有数据,那么我就阻塞等待
- src_addr: 用于确定数据的发送端地址信息
- addrlen: 地址信息的长度
- 返回值:实际接收的数据长度 ,-1:失败
4、发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len,
int flag, struct sockaddr *dest_addr,socklen_t addrlen)
- sockfd: socket描述符,发送数据的时候就是通过这个socked所绑定的地址来发送
- buf: 要发送的数据
- len: 要发送的数据长度
- flag: 0-默认阻塞式发送
- dest_addr: 数据要发送到的对端地址
- addrlen: 地址信息长度
- 返回值:返回实际的发送数据长度,失败返回-1
5、关闭sockfd文件描述符
int close(int fd);
我们说socket函数的返回值是socket描述符,实际也是文件描述符。因为系统的文件描述符有限,我们不再使用这个文件时候,就应该关闭这个描述符,否则可能造成文件描述符泄漏问题。
基于UDP协议的应用层聊天程序
UDP服务端
创建套接字——>绑定地址——>recvfrom——>sendto——>close
//udp服务端
#include<stdio.h>
#include<sys/socket.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
//创建套接字
int sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(sockfd<0)
{
perror("socket error");
return -1;
}
struct sockaddr_in ser_addr;
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(8000);
ser_addr.sin_addr.s_addr=inet_addr("192.168.117.130");
//绑定地址
socklen_t len=sizeof(struct sockaddr_in);
int ret=bind(sockfd,(struct sockaddr*)&ser_addr,len);
if(ret<0)
{
perror("bind error");
close(sockfd);
return -1;
}
//接受数据
while(1)
{
struct sockaddr_in cli_addr;
char buff[1024]={0};
ssize_t recv=recvfrom(sockfd,buff,1023,0,(struct sockaddr*)&cli_addr,&len);
if(recv<0)
{
perror("recvfrom error");
close(sockfd);
return -1;
}
printf("client[ip:%s port:%d] say%s\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),buff);
memset(buff,1024,0x00);
scanf("%s",buff);
sendto(sockfd,buff,sizeof(buff),0,(struct sockaddr*)&cli_addr,len);
}
close(sockfd);
return 0;
}
UDP客户端
客户端程序中,通常我们不推荐手动绑定地址,因为绑定有可能会因为特殊情况失败,但是客户端发送数据的时候,具体用哪个地址和端口我们都无所谓,只要数据能成功发送就可以,所以,客户端程序中我们不手动绑定地址,直到发送数据的时候,操作系统检测到socket没有绑定地址,会自动选择合适的地址和端口为socket绑定,这种绑定方式一般不会出错。
创建套接字——>sendto——>recvfrom——>close
#include<stdio.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>
int main()
{
int sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(sockfd<0)
{
perror("socket error");
return -1;
}
struct sockaddr_in ser_addr;
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(8000);
ser_addr.sin_addr.s_addr=inet_addr("192.168.117.130");
//客户端不推荐绑定地址
while(1) //一旦连接成功就一直通信。直至中断进程
{
socklen_t len=sizeof(struct sockaddr_in);
struct sockaddr_in cliaddr;
char buff[1024]={0};
scanf("%s",buff);
int ret=sendto(sockfd,buff,sizeof(buff),0,(struct sockaddr*)&ser_addr,len);
if(ret<0)
{
perror("sendto error");
close(sockfd);
return -1;
}
memset(buff,1024,0x00);
ssize_t recv=recvfrom(sockfd,buff,1023,0,(struct sockaddr*)&ser_addr,&len);
if(recv<0)
{
perror("recvfrom error\n");
return -1;
}
printf("server[ip:%s port:%d] say%s\n",inet_ntoa(ser_addr.sin_addr),ntohs(ser_addr.sin_port),buff);
}
close(sockfd);
return 0;
}