文章目录:
1.UDP协议
(1)传输层协议
(2)无连接:当UDP客户端想要给UDP服务端发送数据的时候,只需要知道服务端的IP和端口,就可以直接发送;即,在发送数据之前是不知道服务端的状态信息(是否启动 / 是否在跑)
(3)不可靠:不保证UDP的数据一定到达对端机器
(4)面向数据报:UDP数据是整条数据接收和发送的
解释如下:
假设应用层要向传输层传入“hello”,当hello传入传输层还未传入网络层时,应用层又想向传输层传入“world”,此时是不能传输的,只有等“hello”从传输层传入网络层,“world”才能从应用层传入传输层
2. TCP协议
(1)传输层协议
(2)有连接:连接双方在发送数据之前,需要进行连接(需要提前告知,沟通连接的信息)
(3)可靠传输:保证数据是有序并且可靠到达对端
(4)面向字节流:上一次和下一次的数据没有明显的数据边界
解释如下:
假设A机器的应用层先向传输层传入一个“aaa”,再向传输层传入一个“bbb”,到待对端机器的传输层不会区分,是不是一次传过来的
3. UDP的socket编程(流程&接口)
3.1 编程流程
- cs模型(客户端服务端):client-server
- bs模型:浏览器-服务端
3.2 创建套接字
创建套接字的原因:想在内核中(传输层)创建一块与协议相对应的缓冲区,用来保存应用层传入的数据或保存网络层给应用层的数据
int socket(int domain, int type, int protocol);
- domain:指当前的地址域,指定网络层到底使用什么协议
AF_INET:IPV4网络
AF_INET6:IPV6网络
AF_UNIX:本地域套接字- type:创建套接字的类型
SOCK_DGRAM:用户数据报套接字—>UDP协议
SOCK_STREAM:流式套接字—>TCP协议- protocol:
0:表示使用套接字类型默认的协议
IPPROTO_UDP:17
IPPROTO_TCP:6- 返回值:返回套接字描述符,套接字描述符本质上就是一个文件描述符
成功:>= 0
失败: < 0
使用如下:
1 #include<stdio.h>
2 #include<sys/socket.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 int socketfd = socket(AF_INET,SOCK_DGRAM,17);
8 if(socketfd < 0)
9 {
10 perror("socket");
11 return 0;
12 }
13
14 printf("socketfd is %d\n",socketfd);
15 while(1)
16 {
17 sleep(1);
18 }
19 return 0;
20 }
如下图所示,可以看到文件描述符3指向的在闪烁,这是正常情况(指向缓冲区)
3.3 绑定端口号
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- sockfd:socket函数返回的套接字描述符;将创建出来的套接字和网卡,端口进行绑定
- addr:地址信息结构
addr的类型是struct sockaddr ,struct sockaddr 是一个通用地址信息结构,如下图所示:
假设,定义一个int fun(void * x)参数可以接收任何类型数据的函数,使用时就需要强转 char* p = “abc”; fun((void*)lp);而如上结构体的作用相当于此例中的参数,因为bind函数可能绑定 ipv4(uint32_t) / ipv6(uint128_t) / 本地域套接字 等不同类型的协议,所以绑定不同版本的IP地址需要提供不同的绑定函数,而此做法非常的麻烦,所以将协议的数据结构定义为一个通用的,要使用某一具体的协议,只需传入具体的协议对应的数据结构并强转即可
如下图所示,我们可以在 vim /usr/include/netinet/in.h 路径下查看ipv4协议使用的结构体:
图解如下:
域套接字:在本地进行进程间通信用的- addrlen:地址信息结构的长度(告诉网络协议栈最多能解析多少个字节)
3.4 发送接口
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:目标主机的地址信息结构(IP,port)
- addrlen:目标主机地址信息结构的长度
- 返回值:
成功返回具体发送的字节数量
失败返回-1
3.5 接收端口
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
- sockfd:套接字描述符
- buf:将数据接收到buf当中
- len:buf的最大接收能力
- flags:0阻塞接收
- src_addr:这个数据来源的主机的地址信息结构(IP,port)---->由recvfrom()函数填充
- addrlen:输入输出型参数
输入的:在接收之前准备的对端地址信息结构的长度
输出的:实际接收回来的地址信息长度
3.6 关闭套接字
close(int sockfd);
3.7 客户端为什么不推荐绑定地址信息?
本质上是不想让客户端程序将端口写死,即不想让客户端在启动的时候,都是绑定一个端口的(一个端口只能被一个进程所绑定)
eg:客户端A绑定了端口,本机在启动客户端B的时候就会绑定失败
当客户端没有主动的绑定端口,UDP客户端在调用sendto的时候,会自动绑定一个空闲的端口(操作系统分配一个空闲的端口)
3.8 主机字节序<===>网络字节序
(1)主机字节序转换为网络字节序(host to network)
2个字节 uint16_t htons(uint16_t hostshort);
4个字节 uint32_t htonl(uint32_t hostlong);
(2)网络字节序转换为主机字节序
2个字节 uint16_t ntohs(uint16_t netshort);
4个字节 uint32_t ntohl(uint32_t netlong);
3.9 点分十进制IP<===>uint32_t
(1)点分十进制IP转换成为uint32_t
in_addr_t inet_addr(const char *cp);
- 将字符串的点分十进制IP地址转换为uint32_t
- 将uint32_t从主机字节序转换成为网络字节序
(2)uint32_t转换成为点分十进制IP
char *inet_ntoa(struct in_addr in);
- 将网络字节序uint32_t的整数转换成为主机字节序
- 将uint32_t转换成为点分十进制的字符串
4. UDP 客户端 / 服务端的实现
4.1 客户端
客户端只需创建套接字,向服务端发送请求,接收服务端的回复即可
客户端代码如下:
1 #include<stdio.h>
2 #include<string.h>
3 #include<sys/socket.h>
4 #include<unistd.h>
5 #include<netinet/in.h>
6 #include<arpa/inet.h>
7
8 int main()
9 {
10 //创建套接字
11 int sockfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
12 if(sockfd < 0)
13 {
14 perror("socket");
15 return 0;
16 }
17
18
19 while(1)
20 {
21 sleep(1);
22 char buf[1024] = {"i am client"};
23 struct sockaddr_in dest_addr;
24 dest_addr.sin_family = AF_INET;
25 dest_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
26 dest_addr.sin_port = htons(18989);
27 //发送信息
28 ssize_t send_size = sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&dest_addr,sizeof(dest_addr));
29 if(send_size < 0)
30 {
31 perror("sendto");
32 continue;
33 }
34
35
36 //接收信息
37 memset(buf,'\0',sizeof(buf));
38 ssize_t recv_size = recvfrom(sockfd,buf,sizeof(buf)-1,0,NULL,NULL);
39 if(recv_size < 0)
40 {
41 perror("recvfrom");
42 continue;
43 }
44 printf("%s\n",buf);
45 }
46 return 0;
47 }
4.2 服务端
服务端只需创建套接字,绑定端口,接收服务端的请求,回复客户端即可
服务端代码如下:
1 #include<stdio.h>
2 #include<string.h>
3 #include<sys/socket.h>
4 #include<unistd.h>
5 #include<netinet/in.h>
6 #include<arpa/inet.h>
7
8 int main()
9 {
10 //创建套接字
11 int sockfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
12 if(sockfd < 0)
13 {
14 perror("socket");
15 return 0;
16 }
17
18 //绑定端口
19 struct sockaddr_in addr;
20 addr.sin_family = AF_INET;
21 addr.sin_port = htons(18989);
22 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
23 int ret = bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
24 if(ret < 0)
25 {
26 perror("bind");
27 return 0;
28 }
29
30
31 while(1)
32 {
33 //接收信息
34 char buf[1024] = {0};
35 struct sockaddr_in host_addr;
36 socklen_t host_addrlen = sizeof(host_addr);
37 ssize_t recv = recvfrom(sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&host_addr,&host_addrlen);
38 if(recv < 0)
39 {
40 perror("recvfrom");
41 continue;
42 }
43
44 //打印接收到的内容,并打印客户端的IP和端口号
45 printf("i am service,i recv %s,from client IP:%s,port:%d\n",buf,inet_ntoa(host_addr.sin_addr),ntohs(host_addr.sin_por t));
46
47 //发送信息
48 memset(buf,'\0',sizeof(buf));
49 sprintf(buf,"hello!client IP is:%s,port is:%d,i am service!",inet_ntoa(host_addr.sin_addr),ntohs(host_addr.sin_port)) ;
50 ssize_t send_size = sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&host_addr,sizeof(host_addr));
51 if(send_size < 0)
52 {
53 perror("sendto");
54 continue;
55 }
56 }
57 return 0;
58 }
59
4.3 运行效果
将 4.1 和 4.2 的代码运行起来效果如下
客户端:
服务端:
4.4 UDPSocket的封装
为了方便使用,将UDPSocket的各个功能进行封装,面对根据不同的协议和使用场景,使用时只需根据传入不同的参数即可
封装后代码如下:
1 #include<iostream>
2 #include<string.h>
3 #include<unistd.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6 #include<netinet/in.h>
7
8
9 class udp_socket
10 {
11 public:
12 udp_socket():_sockfd(-1)
13 {}
14 public:
15 //创建套接字
16 bool Socket()
17 {
18 _sockfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
19 if(_sockfd < 0)
20 {
21 perror("socket");
22 return false;
23 }
24 return true;
25 }
26 public:
27 //绑定端口
28 bool Bind(const std::string& ip,uint16_t port)
29 {
30 struct sockaddr_in addr;
31 addr.sin_family = AF_INET;
32 addr.sin_port = htons(port);
33 //string类中提供c_str()成员函数,返回char*的字符串
34 addr.sin_addr.s_addr = inet_addr(ip.c_str());
35 int ret = bind(_sockfd,(struct sockaddr*)&addr,sizeof(addr));
36 if(ret < 0)
37 {
38 perror("bind");
39 return 0;
40 }
41 return true;
42 }
43 public:
44 //接收
45 bool Recvfrom(std::string& buf,std::string* ip = NULL,uint16_t* port = NULL)
46 {
47 //接收缓冲区
48 char tem[1024] = {0};
49 //对端地址信息
50 struct sockaddr_in host_addr;
51 socklen_t host_addrlen = sizeof(host_addr);
52 ssize_t recv = recvfrom(_sockfd,tem,sizeof(tem)-1,0,(struct sockaddr*)&host_addr,&host_addrlen);
53 if(recv < 0)
54 {
55 perror("recvfrom");
56 return false;
57 }
58 //将数据从缓冲区取出
59 buf.assign(tem,recv);
60 //获取对端地址信息
61 if(port != NULL)
62 {
63 *port = htons(host_addr.sin_port);
64 }
65 if(ip != NULL)
66 {
67 *ip = inet_ntoa(host_addr.sin_addr);
68 }
69 return true;
70 }
71 public:
72 //发送
73 bool Send(const std::string& buff,const std::string& ip,const uint16_t& port)
74 {
75 struct sockaddr_in addr;
76 addr.sin_family = AF_INET;
77 addr.sin_port = htons(port);
78 addr.sin_addr.s_addr = inet_addr(ip.c_str());
79 ssize_t send_size = sendto(_sockfd,buff.c_str(),buff.size(),0,(struct sockaddr*)&addr,sizeof(addr));
80 if(send_size < 0)
81 {
82 perror("sendto");
83 return false;
84 }
85 return true;
86 }
87 public:
88 void Close()
89 {
90 close(_sockfd);
91 _sockfd = -1;
92 }
93
94 private:
95 int _sockfd;
96 };
查看端口的使用情况:netstat -anp | grep [端口号]