Linux之套接字UDP实现网络通信
文章目录
1.引言
套接字(Socket)是计算机网络中实现网络通信的一种编程接口。它提供了应用程序与网络通信之间的一座桥梁,因为它允许应用程序通过网络发送和接收相应的数据以实现不同主机之间的通信。
通常套接字由以下两部分组成:
1.网络IP和端口号:IP用来标识主机,而端口号可以标识到单台主机的唯一进程。
2.通信协议:套接字通过规定通信协议来制定数据传输和发送的规则。常见的有TCP和UDP等协议。
TCP是一种面向连接 的协议,提供可靠的、有序的、基于字节流的数据传输。
UDP是一种无连接 的协议,提供不可靠的、无序的、基于数据报的数据传输。
我们今天要实现的是通过UDP协议实现网络通信。UDP协议通信虽然无连接不可靠,可是足够简单到我们了解通信的基本原理。
那么话不多讲,我们赶快看看我们学习完今天这一篇能够实现出怎么样的结果吧:
我们通过实现客户端和服务器端,实现了通过套接字UDP创建了一个服务器,之后通过客户端链接并且通信的一个功能。
事不宜迟,我们马上实现!
2.具体实现
首先我们需要明确具体的大思路: 先服务器端创建socket套接字,并recvfrom接收到。客户端也创建套接字绑定后确定到唯一IP和端口号之后即可进行通信。
在具体实现之前我们首先需要一些必要的套接字接口
2.1需要知道的套接字接口
1.socket()
socket函数是用于创建套接字的函数,创建成功返回文件描述符fd,失败返回-1;
int socket(int domain, int type, int protocol);
参数说明:
-
domain
:指定套接字的地址族(Address Family)
今天我们选择:
AF_INET
:IPv4 地址族
-
type
:指定套接字的类型(Socket Type)
今天我们选择:
SOCK_DGRAM
:无连接的数据报套接字,用于 UDP 协议
-
protocol
:可选参数,指定具体的传输协议。常用的有:
今天我们选择:
0
:自动选择合适的协议
2.bind()
在Linux下,bind()
函数用于将一个套接字(socket)与特定的IP地址和端口号进行绑定。
*int bind(int sockfd, const struct sockaddr addr,socklen_t addrlen);
参数说明:
sockfd
:要进行绑定的套接字的文件描述符。addr
:指向一个struct sockaddr
结构体的指针,其中包含要绑定的IP地址和端口号信息。addrlen
:addr
结构体的长度。
在绑定bind的第二个参数中,我们也需要用到库中定义好的sockaddr_in结构体来初始化!
具体结构体struct sockaddr_in说明:
结构体中有三个值也需要初始化指定一下:
sin_family
:表示地址族(Address Family),一般为AF_INET
。
sin_port
:表示端口号。它是一个 16 位的整数,使用网络字节序(大端字节序)表示。在使用时,通常需要使用htons()
函数将主机字节序转换为网络字节序。
sin_addr
:表示 IPv4 地址。它是一个struct in_addr
类型的结构体,用于存储 32 位的 IPv4 地址。一般服务端用INADDR_ANY,让udp_server在启动时候可以绑定任何ip.
客户端用inet_addr函数将字符串转化成32位无符号整数
3.recvfrom()
从套接字接收数据,并获取发送方的地址
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
sockfd
:接收数据的套接字的文件描述符。buf
:指向接收数据的缓冲区。len
:缓冲区的长度。flags
:可选的标志参数,用于影响接收操作的行为,通常设为 0。src_addr
:用于存储发送方的地址信息(对于面向数据报的套接字)。它是一个struct sockaddr
结构体的指针。addrlen
:src_addr
结构体的长度,作为输入参数指定src_addr
缓冲区的大小,作为输出参数返回实际地址的长度。
4.sendto()
通过套接字发送数据到指定目的地
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
:指向目标地址(接收方地址)的结构体指针,可以是struct sockaddr
或其派生类型的指针。addrlen
:dest_addr
结构体的长度。
2.2服务器端server.hpp
在服务器的头文件中,我们首先需要定义一个udpserver的类,服务器类中需要有服务器的初始化与启动命令,当然需要有构造析构等。默认的私有成员是**_sock套接字和port端口**
c++
const static uint16_t default_port = 8080;
class UdpServer
{
public:
UdpServer(uint16_t port = default_port)
:_port(port)
{
std::cout<< "server addr: "<<_port <<std::endl;
}
~UdpServer() {}
void InitServer() //初始化服务器
{
_sock = socket(AF_INET,SOCK_DGRAM,0);
if(_sock < 0)
{
std::cerr << " socket create err " << std::endl;
}
std::cout << "create socket success: " << _sock << std::endl;
struct sockaddr_in local; //利用库中创建好的结构体来初始化socket
memset(&local,0,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 本地主机序列转网络序列
local.sin_addr.s_addr = INADDR_ANY; //让udp_server在启动时候可以绑定任何ip
//绑定
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
std::cerr << " bind error" << std::endl;
exit(-1);
}
std::cout << "bind socket success: " << _sock << std::endl;
}
void Start() //执行逻辑
{
char buffer[1024];
while(true)
{ //接收数据
//ssize_t recvfrom(套接字,缓冲区,缓冲区大小,flag = 0,client的IP和port,实际结构体大小);
struct sockaddr_in far; //远端定义结构体
socklen_t len = sizeof(far);
int n = recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&far,&len);
if(n > 0) buffer[n] = '\0';
else continue;
std::string clientip = inet_ntoa(far.sin_addr); //ipv4的地址从二进制转化为点分十进制的函数
uint16_t clientport = ntohs(far.sin_port); //将网络字节序转化为一个本地主机字节序
std::cout<< clientip << "-" << clientport << "#" << buffer << std::endl;
//发送数据
//ssize_t sendto(套接字,发的数据,数据大小,flag = 0,(struct sockaddr*)&far,sizeof(far));
sendto(_sock,buffer,sizeof(buffer),0,(struct sockaddr*)&far,sizeof(far));
}
}
private:
int _sock; //套接字
uint16_t _port; //端口
};
2.3服务器端server.cc
在服务器端的使用中,我们采用智能指针unique_ptr来帮助资源创建以及销毁,在使用中,我们调用以上server.hpp中类的初始化与启动函数即可.
c++
//输出格式说明:./udp_server port
static void usage(string proc)//使用手册
{
std::cout << "Usage:\n\t" << proc << "port\n" <<std::endl;
}
int main(int argc,char* argv[]) //获取到参数
{
if(argc != 2) //若输入参数不是两个的话,就弹出使用手册
{
usage(argv[0]);
exit(-1);
}
uint16_t port = atoi(argv[1]); //获取到端口直接进行构造后面
std::unique_ptr<UdpServer> ptr(new UdpServer(port));
ptr->InitServer();
ptr->Start();
return 0;
}
2.4客户端Client.cc
在客户端中我们首先需要知道主函数的服务端的ip和端口 ,也就是我们需要从输入的参数来知道服务端是谁?之后由用户输入消息后发送给服务器端并输出。
c++
// 执行格式:./udp_client ip serverport
static void usage(std::string proc) //使用手册
{
std::cout << "Usage:\n\t" << proc << "port\n" <<std::endl;
}
int main(int argc,char* argv[])
{
if(argc != 3) //如果输入参数个数不是3个就弹出使用手册
{
usage(argv[0]);
exit(-1);
}
//从主函数获取到了服务端的ip和端口
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
int sock = socket(AF_INET,SOCK_DGRAM,0); //创建套接字
if(sock < 0)
{
std::cerr << "create socket errno" <<std::endl;
exit(-1);
}
//明确server是谁
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
//这里client一定需要绑定bind 不过由os来帮我们做,因为OS需要随机分配端口,防止冲突
//用户输入
while(true)
{
std::string message;
std::cout<< "please Enter# ";
std::cin >> message;
sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
//接收消息
char buffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int n = recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
if(n > 0)
{
buffer[n] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
最后执行后我们便可以看出结果: 说明执行成功!