UDP 协议通信程序的编写
UDP 协议:无连接、不可靠、基于数据报传输
两端通信流程:
套接字相关接口介绍
#include <sys/socket.h> 头文件
一、 创建套接字
int socket(int domain, int type, int protocol);
(1) domain:地址域类型(域间通信、ipv4通信、ipv6通信… 不同通信有不同的地址结构)
(2) type:套接字类型
SOCK_STREAM:流式套接字,提供字节流传输,默认协议是 TCP 协议
SOCK_DGRAM:数据报套接字,提供的是数据报传输,默认协议是 UDP 协议
(3) protocol:协议类型,默认使用 0,表示使用套接字类型对应的默认协议
IPPROTO_TCP:值为 6
IPPROTO_UDP:值为 17
返回值:成功返回一个套接字描述符,失败返回 -1
二、为套接字绑定地址信息
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
(1) sockfd:socket 返回的套接字描述符
(2) my_addr:要绑定的地址信息
但是不同地址域类型具有不同的地址结构
ipv4 地址域类型 2B;端口号 2B;IP地址 4B
ipv6 地址域类型 2B;端口号 4B;IP地址 4B
(3) addrlen:绑定地址信息的长度
返回值:成功返回 0 ,失败返回 -1
三、发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
(1) sockfd:socket 返回的套接字描述符
(2) buf:要发送的数据空间起始地址
(3) len:要发送的数据长度,从 buf 地址开始,发送 len 长度的数据
(4) flags:默认 0-阻塞发送(发送缓冲区数据满了则等待)
(5) dest_addr:对端地址信息,描述数据要发送给谁(设置地址信息)
(6) addrlen:对端地址信息长度
返回值:成功返回实际发送数据字节长度,失败返回 -1
四、接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
(1) sockfd:socket 返回的套接字描述符
(2) buf:空间起始位置,接收到的数据存放在 buf 空间中
(3) len:想要接收到的数据长度
(4) flags:默认 0-阻塞接收(socket 接收缓冲区中没有数据则阻塞,直到有数据)
(5) src_addr:接收数据的同时,需要知道数据是谁发的,(获取地址信息)输出参数,用于返回对端地址信息
(6) addrlen:地址信息长度,输入输出参数,表示想要接收的地址信息长度以及实际得到的地址信息长度
返回值:成功返回实际接收到的数据长度,失败返回 -1
五、关闭套接字,释放资源
int close(int fd);
字节序相关接口
注意 32 位数据转换接口与 16 位数据转换接口不可混用
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
主机字节序到网络字节序的转换
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
网络字节序到主机字节序的转换
示例说明:
IP 地址转换接口
in_addr_t inet_addr(const char *cp); 将点分十进制IP地址转换为网络字节序整型IP地址
const char *inet_ntoa(struct in_addr in); 将网络字节序整型IP地址转换为点分十进制IP地址
inet_ntoa 接口,若返回的是一个局部变量(函数退出就会释放),这个函数返回的是静态变量地址
测试代码:
服务端:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4 #include<unistd.h>
5 #include <arpa/inet.h> //字节序转换接口头文件
6 #include <sys/socket.h> //套接字接口头文件
7 #include <netinet/in.h> //地址结构类型以及协议类型宏头文件
8
9 int main(int argc,char* argv[])
10 {
11 if(argc!=3){
12 printf("./udp_ser 192.168.2.2 9000\n");
13 return -1;
14 }
15 uint16_t port=atoi(argv[2]); //端口信息,uint16 属于 ipv4 数据类型----包含有2字节16位数据 ,atoi 将字符串转换为整数
16 char* ip=argv[1] ; //ip 地址信息
17
18 //1、创建套接字
19 //int socket(int domain, int type, int protocol);
20 int sockfd=socket(AF_INET ,SOCK_DGRAM,IPPROTO_UDP);
21 //AF_INET 代表的是 ipv4 地址域类型,SOCK_DGRAM 表示数据报套接字-UDP,IPPROTO_UDP 表示 UDP 协议类型
22
23 if(sockfd<0){
24 perror("socket error!\n");
25 return -1;
26 }
27
28
29 //2、为套接字绑定地址信息
30 //int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
31
32 struct sockaddr_in addr; //定义一个 IPV4 地址结构
33 addr.sin_family=AF_INET; //定义为 IPV4 地址域类型
34 addr.sin_port=htons(port); //ipv4 地址域端口为 uint16_t 对应 16 位----使用 htons 进行主机字节序端口到网络字节序端口的转换
35 addr.sin_addr.s_addr=inet_addr(ip); //将点分十进制字符串 IP 地址转换为整型IP地址
36 socklen_t len=sizeof(struct sockaddr_in); //计算地址信息长度
37
38 int ret=bind(sockfd,(struct sockaddr*)&addr,len); //传递地址信息需要进行类型强转
39 if(ret<0){
40 perror("bind error!\n"); //套接字绑定地址信息失败
41 return -1;
42 }
43
44 //3、循环接收发送数据
45 while(1){
46 //接收信息
47 //ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
48 char buff[1024]={0}; //接收到的信息存放地址
49 struct sockaddr_in peer; //从哪里接收到的信息
50 socklen_t len=sizeof(struct sockaddr_in); //接受的数据长度
51
52 ssize_t ret= recvfrom(sockfd,buff,1023,0,(struct sockaddr*)&peer,&len); //0-阻塞
53 if(ret<0){
54 perror("recvfrom error!\n");
55 return -1;
56 }
57
58
59 char* peerip=inet_ntoa(peer.sin_addr); //数据发送到哪里,将整形 IP 地址转换为点分十进制的 IP 地址
60 uint16_t peerport=ntohs(peer.sin_port); //ipv4 地址域类型为 uint16_t 含有16 位数字-->ntohs,将网络字节端口转换为主机字节序端口
61
62 printf("client[%s : %d] say:%s",peerip,peerport,buff);
63
64
65 //发送信息
66 //ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
67 char data[1024]={0} ; //要发送的数据存放位置
68 printf("server say: ");
69 fflush(stdout);
70 scanf("%s",data);
71
72 ret=sendto(sockfd,data,strlen(data),0,(struct sockaddr*)&peer,len); //0-阻塞
73 if(ret<0){
74 perror("sendto error!\n");
75 return -1;
76 }
77
78 }
79 //关闭套接字
80 close(sockfd);
81
82 return 0;
83 }
进行绑定(bind)端口时候:
虚拟机绑定的是 ens33 处的网卡信息
云服务器绑定的是 etho 处的网卡信息
端口是一个 uint16_t 类型的数据,范围 0~65535
但是 0~1023 这些端口被一些知名服务使用,例如 ssh–22号,http–80号端口
因此在使用时候尽量使用 1024 以上的端口,防止端口冲突导致绑定失败。
对于服务端或客户端都会存在:
创建套接字、绑定地址(客户端不需要绑定)、发送数据、接收数据、关闭套接字,因此我们可以将套接字的一系列接口进行封装来使用:
1 #include <iostream>
2 #include<assert.h>
3 #include<unistd.h>
4 #include<string>
5 #include <cassert>
6 #include<sys/socket.h>
7 #include<arpa/inet.h> //字节序转换接口头文件
8 #include<netinet/in.h> //地址结构类型以及协议类型宏头文件
9
10 //进行 UDP 接口的封装
11
12 class UdpSocket{
13 private:
14 int _sockfd;
15 public:
16 UdpSocket():_sockfd(-1) {}
17 ~UdpSocket() {
18 Close();
19 }
20
21 //创建套接字
22 bool Socket(){
23 _sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//定义为 IPV4 地址域,数据报传输(UDP),定义传输协议为 UDP 协议
24 if(_sockfd<0){
25 perror("socket error!\n");
26 return false;
27 }
28 return true; //创建套接字成功
29 }
30
31 //给套接字绑定地址空间
32 bool Bind(const std::string& ip,uint16_t port)
33 {
34 //创建 ipv4 地址域结构 struct sockaddr_in 类型
35 struct sockaddr_in addr;
36 addr.sin_family=AF_INET; //ipv4 地址域类型
37 addr.sin_port=htons(port); //将主机字节序转换为网络字节序---16位使用 htons
38 addr.sin_addr.s_addr=inet_addr(ip.c_str()); //将 IP 地址转换为整型 IP
39 socklen_t len=sizeof(struct sockaddr_in);
40
41 int ret=bind(_sockfd,(struct sockaddr*)&addr,len);
42 if(ret<0){
43 perror("bind error!\n");
44 return false;
45 }
46 return true;
47 }
48
49 //接收数据
50 bool Recv(std::string& body,std::string* peer_ip = NULL,uint16_t* peer_port=NULL)
51 {
52 struct sockaddr_in peer;
53 socklen_t len=sizeof(struct sockaddr_in);
54 char tmp[4096]={0};
55 ssize_t ret=recvfrom(_sockfd,tmp,4096,0,(struct sockaddr*)&peer,&len); //0-阻塞接收
56 if(ret<0){
57 perror("recvfrom error!\n");
58 return false;
59 }
60 if(peer_ip!=NULL)
61 *peer_ip=inet_ntoa(peer.sin_addr); //获取到源端IP信息,转换为点分十进制IP地址
62 if(peer_port!=NULL)
63 *peer_port=ntohs(peer.sin_port); //将网络套接字转换为主机套接字
64 body.assign(tmp,len); //从 tmp 获取 len 长度字节放入到 body 中
65 return true;
66 }
67
68
69 //发送数据
70 bool Send(const std::string& body,const std::string& peer_ip,uint16_t peer_port )
71 {
72 struct sockaddr_in addr;
73 addr.sin_family=AF_INET;
74 addr.sin_port=htons(peer_port); //将点云十进制 IP 转换为整型 IP
75 addr.sin_addr.s_addr=inet_addr(peer_ip.c_str()); //将主机字节序转换为网络字节序
76 socklen_t len=sizeof(struct sockaddr_in);
77
78 ssize_t ret=sendto(_sockfd,body.c_str(),body.size(),0,(struct sockaddr*)&addr,len);
79 if(ret<0){
80 perror("sendto error!\n");
81 return false;
82 }
83 return true;
84 }
85
86 //关闭套接字
87 bool Close()
88 {
89 if(_sockfd!=-1){
90 close(_sockfd);
91 _sockfd=-1;
92 }
93 return true;
94 }
95 };
运行结果显示:
网络通信程序进行通信时候有可能是跨主机的,因此会涉及到防火墙设置:
(虚拟机)需要先停用防火墙才能进行通信
1、sudo systemctl stop firewalld 停止防火墙服务
2、sudo systemctl diable firewalld 禁用防火墙(下次开始依然不会重启)
虚拟机只能实现与自己电脑、或虚拟机内部通信,无法实现跨主机(因为虚拟机IP是私有地址且对外隐藏)
NAT 地址转换技术----将私网内的主机对外发送的数据的源端地址进行替换为对外地址
(云服务器)登录云服务器网站,进行安全策略组设置,开通指定要访问的接口,才能实现对外访问
TCP 协议通信程序的编写
TCP :面向连接、可靠传输、面向字节流的传输
因为 TCP 是面向连接的,在进行通信之前必须先建立连接(一对一),因此在 TCP 通信中不限定必须是客户端先进行发送数据
TCP 通信程序中:
客户端向服务器发送一个连接建立请求,服务端处于监听状态,则会对这个连接建立请求进行处理----> (1)为这个新连接请求,创建一个套接字结构体;(2)为这个新的套接字,描述完整的五元组信息;往后的数据通信就由这个套接字进行通信
一个服务器上有多少客户端想要建立连接,服务端就要创建多少个套接字(源端IP及端口都是一样的);而最早的服务端创建监听套接字则只负责新连接请求处理,不负责数据的通信
服务端会为每个客户端都创建一个新的套接字,负责与这个客户端进行数据通信,但是想要通过这个套接字与客户端进行通信就必须知道这个套接字的描述符
通信流程:
套接字接口介绍
一、创建套接字
int socket(int domain, int type, int protocol);
(1)domain:地址域类型-----ipv4 通信 AF_INET
(2)type:套接字类型–字节流传输 – SOCK_STREAM
(3)protocol:协议类型: IPPROTO_TCP
二、建立连接
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
(1)sockfd:socket 返回的套接字描述符
(2)my_addr:要绑定的地址信息
(3)addrlen:要绑定的地址信息长度
返回值:成功返回 0,失败返回 -1
三、开始监听
int listen(int socket, int backlog);
(1)socket:创建套接字返回的监听套接字描述符
(2)backlog:同一时刻最大并发连接数(限制同一时间有多少个客户端连接请求能够被处理-----否则会导致系统崩溃(连续创建多个套接字导致系统资源耗尽))
返回值:成功返回 0 ,失败返回 -1
-内核中每个监听套接字都有一个对应新连接的socket队列:半连接队列 / 已完成连接队列 (半连接队列被放满,当在有新的连接需要建立时会被直接丢弃)
-listen 函数第二个参数 backlog 限制的就是队列中节点数量 = backlog + 1
泛洪攻击------SYN cookies (自行调研)
四、向服务端发送连接建立请求(客户端使用)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
(1)sockfd:套接字描述符
(2)addr:服务端地址信息
(3)addrlen:地址长度
五、获取新建连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
从内核 sockfd 指定的监听套接字对应的已完成连接队列中,取出一个 socket 并返回其描述符,通过 addr 参数返回值可以知道具体连接请求来源于哪个客户端
(1)sockfd:套接字描述符,是我们创建的套接字 sockfd
(2)addr:accept 接收到的填充客户端地址----输出参数
(3)addrlen:地址信息长度–输入输出参数,用于指定想要获取的地址长度以及返回实际地址信息长度
返回值:成功返回新连接的套接字描述符,出错返回 -1
六、收发数据
int send(int sockfd, const void *msg, size_t len, int flags);
sockfd 是 accept 函数中返回的新建连接的 sockfd
相较于 sendto 不需要指定 IP 和端口信息
返回值:成功返回实际发送数据的长度,失败返回 -1
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd 是 accept 函数中返回的新建连接的 sockfd
相较于 recvfrom 不需要获取 IP 和端口信息
返回值:成功返回实际发送的字节长度,出错返回 -1 ,连接断开返回 0
recv 返回值为 0 ,主要是告诉程序员连接断开了
TCP 是面向连接的,一旦连接断开将无法实现通信(对方关闭了连接/网络出错)
七、关闭套接字
int close(int fd);
测试代码:
对 TCP 相关接口进行封装
1 #include<iostream>
2 #include<string>
3 #include<stdio.h>
4 #include<unistd.h>
5 #include <sys/socket.h>
6 #include <arpa/inet.h>
7 #include <netinet/in.h>
8
9 #define BACKLOG 1024 //宏定义:同一时刻最大客户端建立连接数
10
11 //封装一个 TCP 接口
12 class TcpSocket{
13 private:
14 int _sockfd;
15 public:
16 TcpSocket():_sockfd(-1) {}
17 ~TcpSocket(){
18 Close();
19 _sockfd=-1;
20 }
21
22 //创建套接字:TCP 协议
23 bool Socket()
24 { //SOCK_STREAM :字节流传输 ,IPPROTO_TCP :表示使用 TCP 协议
25 _sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
26 if(_sockfd<0){
27 perror("socket error!\n");
28 return false;
29 }
30 return true;
31 }
32 //绑定端口
33 bool Bind(const std::string &ip,uint16_t port)
34 { //建立 ipv4 地址结构
35 struct sockaddr_in addr; //ipv4 地址结构
36 addr.sin_family=AF_INET; //ipv4 地址域
37 addr.sin_port=htons(port); //将主机套接字转换为网络套接字
38 addr.sin_addr.s_addr=inet_addr(ip.c_str()); //将地址信息转化为整型 IP 地址
39 socklen_t len=sizeof(struct sockaddr_in);
40
41 int ret=bind(_sockfd,(struct sockaddr*)&addr,len);
42 if(ret<0){
43 perror("bind error!\n");
44 return false;
45 }
46 return true;
47 }
48
49 //开始监听
50 bool Listen(int backlog = BACKLOG)
51 {
52 int ret=listen(_sockfd,backlog);
53 if(ret<0){
54 perror("listen error!\n");
55 return false;
56 }
57 return true;
58 }
59
60 //建立连接:客户端申请向服务端进行连接(只要客户端需要该接口)
61 bool Connect(const std::string& srvip,uint16_t srvport)
62 {
63 struct sockaddr_in addr;
64 addr.sin_family=AF_INET;
65 addr.sin_port=htons(srvport); //主机字节序转换为网络字节序
66 addr.sin_addr.s_addr=inet_addr(srvip.c_str()); //将地址信息转化为整型 IP 地址
67 socklen_t len=sizeof(struct sockaddr_in);
68
69 int ret=connect(_sockfd,(struct sockaddr*)&addr,len);
70 if(ret<0){
71 perror("connect error!\n");
72 return false;
73 }
74 return true;
75 }
76
77 //接收连接,获取新建连接的客户端地址信息
78 bool Accept(TcpSocket* new_sock,std::string* cli_ip=NULL,uint16_t*cli_port=NULL)
79 {
80 struct sockaddr_in addr;
81 socklen_t len=sizeof(struct sockaddr_in);
82 int newfd=accept(_sockfd,(struct sockaddr*)&addr,&len);
83 if(newfd<0){
84 perror("accept error!\n");
85 return false;
86 }
87 new_sock->_sockfd=newfd; //新连接的套接字描述符
88 if(cli_ip != NULL)
89 *cli_ip=inet_ntoa(addr.sin_addr); //将获取到的客户端 IP 地址转换为整型
90 if(cli_port!=NULL)
91 *cli_port=ntohs(addr.sin_port);//将客户端端口从网络套接字 IP 转换为主机套接字(因为 ipv4 地址域类型为 uint16 含有两字节因此使用 ntohs )
92 return true;
93 }
94
95 //发送数据
96 bool Send(const std::string& body)
97 {
98 ssize_t ret=send(_sockfd,body.c_str(),body.size(),0); //0-阻塞等待,此处 _sockfd 为已经建立连接的套接字描述符
99 if(ret<0){
100 perror("send error!\n");
101 return false;
102 }
103 return true;
104 }
105
106
107 //接收数据
108 bool Recv(std::string* body)
109 {
110 char tmp[1024]={0};
111 //recv 返回值大于 0;等于 0(表示断开连接);小于0(表示出错)
112 ssize_t ret =recv(_sockfd,tmp,1023,0); //0-阻塞等待 ,此处 _sockfd 为已经建立连接的套接字描述符
113 if(ret<0){
114 perror("recv error!\n");
115 return false;
116 }
117 else if(ret==0){ //连接断开
118 printf("connect shutdown!\n");
119 return false;
120 }
121 body->assign(tmp,ret); //从 tmp 截取 ret 长度数据放到 body
122 return true;
123 }
124
125 //关闭套接字
126 bool Close()
127 {
128 if(_sockfd!=-1){
129 close(_sockfd);
130 _sockfd=-1;
131 }
132 return true;
133 }
134 };
在客户端与服务端我们进行调用所封装的 TcpSocket 类来实现套接字创建、建立连接、接收连接、收发数据、关闭套接字信息等。
但是在代码运行过程中我们会发现一些问题:
问题一:
服务端 TcpSocket new_sock 是一个局部变量,while 循环一次之后就会被释放析构,因此在服务端接收数据时候会发现 recv 返回值为 0 - 连接断开(connect shutdown 信息提示)
当我们将封装好的 TcpSocket 类内析构函数中将关闭套接字接口去掉之后,又发现一个新问题-------> 客户端只能与服务端建立一次通信:
这主要是因为 TcpSocket new_sock 是一个局部变量,每一次循环进入之后都会创建一个新套接字,导致只能接受新连接(新建立的客户端的通信)的一次通信,因此导致一个客户端无法与服务端建立连续的通信------------------那么,我们考虑能否将收发数据变为循环进行的,从而实现同一个客户端与服务端的连续通信
问题二:
在服务端内部收发数据部分添加 while 循环时发现可以建立多次通信,但是多个客户端运行时只有一个客户端能够与服务器建立通信,而其他客户端不能进行与服务器的通信
而当我们关闭第一个客户端的连接之后,发现服务端卡死:
这主要是因为,我们在服务端循环收发数据时候使用的是 continue ,当接收/发送数据失败时候,跳出当前循环等待下一次接收/发送数据,从而导致了服务端卡死,因此我们需要将 continue 变为 break 跳出本次循环,结束当前的数据收发:
本质原因:
多执行流要进行多任务处理有两种方案:
多进程 & 多线程
多进程:安全、健壮
多线程:通信灵活、消耗小
多进程实现多执行流
我们采用多进程来解决以上问题:
主进程只负责一件事----->获取新连接,获取成功则创建一个新的子进程,让子进程来实现与客户端的通信
但是要考虑子进程退出形成僵尸进程,因此需要忽略子进程退出信号:
此时,多个客户端可以完成与服务端的正常通信:
注意:
在创建子进程之后,要考虑的第一个问题就是僵尸进程—子进程退出,为了保存退出返回值而导致子进程资源没有完全释放,因此我们进行了一个信号的忽略处理 signal(SIGCHLD,SIG_IGN)
同时还需考虑一个问题:子进程创建 fork() 之后会导致父子进程代码共享数据独有,子进程来负责与客户端的通信,而父进程负责创建新建连接,但是在父进程这边,只需要创建新建连接获取新建连接的套接字描述符,在后期父进程并未使用该套接字描述符,因此会存在一个内存泄漏问题(由于之前我们去除掉了析构函数中的套接字关闭接口),因此在子进程进行收发数据失败之后只关闭了子进程中的套接字描述符,而父进程中没有关闭
所以,在父进程的运行中,我们需要调用套接字关闭接口来避免内存泄漏
多线程实现多执行流
将服务端的数据收发处理交给不同的线程来进行处理,因此在完成一个新建连接之后,我们需要进行线程的创建,成功创建则进行数据收发的处理
服务端代码:
(在这段代码中我们采用的是类型强制转换,因此运行时会有一个警告)
添加链接描述
改进:
使用指针类型参数实现:
在外面运行程序时,常会有一个提示说绑定地址失败:
主要是因为当前这个端口被占用
netstat -anptu:
查看当前主机上所有网络连接状态
a:all 所有连接信息
n : 以 IP 地址和端口号显示,而不要用服务名称来显示—> 127.0.0.1 – localhost ,22 – ssh 端口
p:显示连接对应的进程 ID 和 名称
t:tcp 套接字信息
u:udp 套接字信息
欢迎点赞留言鸭!!
文章评论