Linux从系统到网络|socket(套接字)实现udp通信


udp通信

  • 储备知识
  • 网络字节序
  • udp使用的接口
    • sockaddr结构
  • 简单的udp通信
    • 优化服务器

储备知识
源ip地址和目的ip地址
我们先来看个例子:
Linux从系统到网络|socket(套接字)实现udp通信
文章图片

如果当女儿国国王问你上一站从何而来,下一站去往何处?唐僧就会说我上一站从XXX来下一站到XXX。唐僧总是有2套说辞。源ip地址就像是唐僧的上一站,目的ip就是下一站的地址。
源ip地址:就是发送数据包的那个电脑的IP地址。
目的ip地址: 就是想要发送到的那个电脑的IP地址。
端口号
那我们有了ip地址就能通信了吗?例如QQ发消息,我们有了ip地址能够把信息发给对方的机器上,但是我们还需要有一个其他的标识来区分出这个数据交给哪个程序来进行解析。
下面来简单认识一下端口号:
  • 端口号是是一个2字节16位的整数
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
  • 一个端口号只能被一个进程占用
ip来标识主机,端口号标识进程,ip+端口号就可以标识全网的唯一进程,我们就可以知道数据要交给哪个程序解析了。一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。
源端口号和目的端口号
和上面的源ip地址、目的ip地址一样的,传输层协议的数据段中有2个端口号,源端口号:数据是谁发的,目的端口号:要发给谁
udp协议:先简单认识一下,后面有详细的讲解
  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报
网络字节序 内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
下面有一批接口可以使用:
#include uint32_t htonl(uint32_t hostlong); uint16_t htonl(uint16_t hostshort); uint32_t htonl(uint32_t netlong); uint16_t htonl(uint16_t netshort);

详细解释:
  • h表示host,n表示network,l表示32位长整数,s表示16位短整数
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
udp使用的接口 先来认识udp的一套接口:
1.创建socket
int socket(int domain, int type, int protocol);

参数说明:
domain:协议域又称协议家族,协议族决定了socket的地址类型,我们使用ipv4进行通信,使用AF_INET
type:套接字类别,有流式套接字和数据报套接字,upd使用的是SOCK_DGRAM
protocol:协议指定与套接字一起使用的特定协议。默认使用0即可。
返回值:
成功则返回socket文件描述符,错误返回-1.
Linux从系统到网络|socket(套接字)实现udp通信
文章图片

为什么返回文件描述符?
Linux中说一切皆文件,为了表示和区分已经打开的文件,Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:
通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。
【Linux从系统到网络|socket(套接字)实现udp通信】Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
所以网络连接也是一个文件,也要有文件描述符,所以它的返回值是文件描述符。
2.bind(绑定函数)
函数原型:
int bind(int socket, const struct sockaddr *address,socklen_t address_len);

参数说明:
socket:需要绑定的socket
addr:存放了服务端用于通信的地址和端口。
addrlen:表示addr结构体的大小。
返回值:成功返回0,失败返回-1
3.recvfrom(接收)
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:指向结构体长度值
  • 注意后两个参数是输出参数,其中addrlen既是输入又是输出参数,即值-结果参数,需要在调用时,指明src_addr的长度。另外,如果不关心数据发送端的地址,可以将后两者均设置为NULL。
返回值:如果正确接收返回接收到的字节数,失败返回-1.
4.sendto(发送数据)
函数原型:
ssize_t sendto(int sockfd, const void *buf, size_t len,int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明:
  • sockfd:socket文件描述字
  • buf:指明一个存放应用程序要发送数据的缓冲区
  • len:指明buf的长度
  • flag:一般设置为0
  • dest_addr:表示目的机的地址和端口号信息
  • addrlen:常被赋值为sizeof(struct sockaddr)
sockaddr结构 socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。上面的接口都是使用sockaddr结构,先来看一下它的结构:
Linux从系统到网络|socket(套接字)实现udp通信
文章图片

它们的接口使用的都是struct sockaddr,是为了使用一套接口就可以完成通信。但是我们在使用的时候使用的是第2个,因为我们实现通信就要传ip地址和端口号,哪个8字节填充是为了内存对齐,不用管它。那怎么区别它们呢?
只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好
处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
sockaddr_in结构
Linux从系统到网络|socket(套接字)实现udp通信
文章图片

下面的填充的不用管。
in_addr结构
Linux从系统到网络|socket(套接字)实现udp通信
文章图片

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。
简单的udp通信 知道了上面的接口,下面我们使用这些接口写一个简单的udp通信。
服务器端:
我们让服务器一直读客户端发来的信息,并给服务器返回。整体的逻辑:先创建套接字,绑定,接收客户端信息,给客户端返回信息。
udpServer.hpp:
1 #pragma once 2 #include 3 #include 4 #include> 5 #include 6 #include/socket.h> 7 #include.h> 8 #include/types.h> 9 #include 10 #include 11 12 class udpServer 13 { 14private: 15std::string ip; 16int port; 17int sock; 18public: 19udpServer(std::string _ip="127.0.0.1",int _port = 8088) 20:ip(_ip) 21,port(_port) 22{} 23void initServer() 24{ 25sock = socket(AF_INET,SOCK_DGRAM,0); 26std::cout<<"sock:"<<::endl; 27struct sockaddr_in local; 28local.sin_family = AF_INET; 29local.sin_port = htons(port); //主机序列转成网络序列 30local.sin_addr.s_addr = inet_addr(ip.c_str()); 31 32//开始绑定 33//绑定失败就直接退出 34if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0) 35{ 36std::cerr<<"bind error"<::endl; 37exit(1); 38 39} 40} 41void start() 42{ 43char msg[64]; 44for(; ; ) 45{ 46msg[0] = '\0'; 47//从远端接收 48struct sockaddr_in end_point; 49socklen_t len = sizeof(end_point); 50ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len); 51if(s > 0 ) 52{ 53msg[s] = '\0'; 54std::cout<<"client:"<

udpServer.cc
1 #include"udpServer.hpp" 2 3 int main() 4 { 5udpServer *us = new udpServer(); 6us->initServer(); 7us->start(); 8delete us; 9return 0; 10 }

客户端发消息给服务器端,服务器端返回echo server
udpClient,hpp
1 #pragma once 2 #include 3 #include 4 #include 5 #include/socket.h> 6 #include.h> 7 #include/types.h> 8 #include 9 #include> 10 #include 11 class udpClient 12 { 13private: 14std::string ip; 15int port; 16int sock; 17public: 18udpClient(std::string _ip="127.0.0.1",int _port = 8088) 19:ip(_ip) 20,port(_port) 21{} 22void initClient() 23{ 24sock = socket(AF_INET,SOCK_DGRAM,0); 25std::cout<<"sock:"<<::endl; 26 27} 28void start() 29{ 30 31std::string msg; 32struct sockaddr_in peer; 33peer.sin_family = AF_INET; 34peer.sin_port = htons(port); 35peer.sin_addr.s_addr = inet_addr(ip.c_str()); 36for(; ; ) 37{ 38std::cout<<"please enter:"; 39std::cin>>msg; 40if(msg == "quit") 41{ 42break; 43} 44sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer)); 45char echo[128]; 46ssize_t s = recvfrom(sock,echo,sizeof(echo)-1,0,nullptr,nullptr); 47if(s > 0) 48{ 49echo[s] = 0; 50std::cout<<"server:"<

udpClient.cc
1 #include"udpClient.hpp" 2 3 int main() 4 { 5udpClient uc; 6uc.initClient(); 7uc.start(); 8return 0; 9 }

下面开始通信
Linux从系统到网络|socket(套接字)实现udp通信
文章图片

我们可以看到通信成功了,服务器给我们返回echo server。
优化服务器
12 class udpServer 13 { 14private: 15// std::string ip; 16int port; 17int sock; 18public: 19udpServer(int _port = 8088) 20//:ip(_ip) 21:port(_port) 22{} 23void initServer() 24{ 25sock = socket(AF_INET,SOCK_DGRAM,0); 26std::cout<<"sock:"<<::endl; 27struct sockaddr_in local; 28local.sin_family = AF_INET; 29local.sin_port = htons(port); //主机序列转成网络序列 30//local.sin_addr.s_addr = inet_addr(ip.c_str()); 31local.sin_addr.s_addr = INADDR_ANY;

将sin.addr设为INADDR_ANY;
运行服务器:
用命令:netstat -nlup查看udp协议相关的统计数据,一般用于检验本机各端口的网络连接情况
Linux从系统到网络|socket(套接字)实现udp通信
文章图片

转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。所以出现INADDR_ANY,你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。
客户端和服务器端都加上命令行参数
服务器端:
1 #include"udpServer.hpp" 2 3 void Usage(std::string proc) 4 { 5std::cout<<"Usage: "start(); 17delete us; 18return 0; 19 }

客户端:
1 #include"udpServer.hpp" 2 3 void Usage(std::string proc) 4 { 5std::cout<<"Usage: "start(); 17delete us; 18return 0; 19 }

效果演示:
Linux从系统到网络|socket(套接字)实现udp通信
文章图片

可以绑定127.0.0.1本地环回网,和ifconfig查看的ip都可以进行通信。
小结:
客户端不需要绑定。为什么呢?
  • 客户端自己bind容易冲突,如果有多个客户端进行bind,我们自己不知道哪个端口号被bind,肯定会有客户端启动失败。
  • 客户端需要唯一性,但是不需要明确是哪个端口号,所以客户端由操作系统自己选择绑定,因为系统知道哪个端口被bind,哪个端口没被bind。
本地环回:
通常用来进行网络通信代码的本地测试,一般跑通,本地环境以及代码基本没有问题。
也可以远程通信,代码链接,点击直达。
有客户端代码就可以和博主远程通信了。

    推荐阅读