Linux—套接字编程(一):UDP/TCP协议,UDPSocket编程(流程+接口+封装),UDP客户端/服务端的实现


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);

  • sockfdsocket函数返回的套接字描述符;将创建出来的套接字和网卡,端口进行绑定
  • 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 [端口号]

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值