目录
1、实现服务器的逻辑
1.1 socket
1.2 bind
1.3 recvfrom
1.4 sendto
1.5 服务器代码
2、实现客户端的逻辑
2.1 客户端代码
3、实现通信
结语
前言:
在Linux下,实现传输层协议为UDP的套接字进行网络通信,网络层协议为IPv4,需要用到的接口有以下4个:socket、bind、recvfrom、sendto。具体实现方法:在云服务器上创建一个服务器进程和一个客户端进程,让客户端向服务器发送消息,并且服务器收到消息后可以反馈给对方。
示意图如下:
1、实现服务器的逻辑
按照以下函数的调用顺序,即可实现服务器方的UDP通信。
1.1 socket
首先明确使用IPv4协议和UDP协议后,先调用接口socket,让其返回一个网络文件描述符给到我们,socket函数介绍如下:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//domain表示网络协议族,AF_INET为IPv4,AF_INET6为IPv6
//type表示传输层协议,SOCK_STREAM为TCP,SOCK_DGRAM为UDP
//protocol表示指定特定的协议,一般前两个参数的协议足矣,这里填0即可
//调用成功返回一个类型文件描述符的网络描述符,失败返回-1
1.2 bind
定义一个struct sockaddr_in类型的变量,该变量的作用是为调用bind接口做准备,该变量里面有3个信息需要填写,分别是:1、传输层协议,2、为该进程设置的端口号,3、该主机的IP地址。其中,端口号和IP地址需要对其转换成大端字节序,因为网络规定传输的数据采用大端字节序传输,这里介绍两个接口可以帮助我们直接将端口号和IP转换成大端字节序,接口介绍如下:
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);//常用来转换端口号
//将主机字节序转换成网络字节序并返回
#include <arpa/inet.h>
unsigned long inet_addr(const char *cp);//常用来转换IP地址
//如果cp指向的是IP地址的字符串形式,那么会将其转换为网络字节序的IP地址
//并且以无符号的长整型返回
待struct sockaddr_in类型的变量的字段填写完毕后,下一步就是进行绑定操作,绑定的目的是将socket返回的网络描述符与struct sockaddr_in类型的变量进行绑定,即可以通过网络描述符来找到对应的ip地址以及端口号,简单来说,通过网络描述符就能找到对应主机的对应进程。
接着就是调用bind接口进行绑定了,bind接口介绍如下:
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
//socket表示要绑定的网络描述符
//address表示指向struct sockaddr类型的变量
//address_len表示address指向变量的大小
//调用成功返回0,失败返回-1
1.3 recvfrom
recvfrom接口有点类似文件操作中的read接口,都带有接收的意思。recvfrom接口用于从网络描述符中读取对方主机发送的数据,并且还可以将对方主机的地址信息(IP和端口号)给记录下来,该接口的介绍如下:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
//sockfd表示读取的网络描述符
//buf表示存放读取数据的目标缓冲区
//len表示期望读取内容的大小
//flag表示设置该函数的模式,比如阻塞或非阻塞,通常设为0表阻塞
//src_addr是个输出型参数,用于保存发送方的地址信息
//addrlen表示src_addr指向变量的大小
//成功返回接收的字节数,若sockfd关闭返回0,失败返回-1
1.4 sendto
如果说recvfrom接口类似read接口,那么sendto就好比write接口,他能够往网络文件描述符内写入数据,即发送方就是调用sendto接口向接收方发送数据,sendto和recvfrom相互搭配实现网络通信。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表示要发送数据的长度
//flag表示设置该函数的模式,比如阻塞或非阻塞,通常设为0表阻塞
//dest_addr指向的结构体里包含接收方的IP和端口号,依靠他们才能找到接收方
//addrlen表示dest_addr指向结构体的大小
1.5 服务器代码
将服务器封装成一个类,并把服务器的端口号、ip地址、网络描述符作为该类的成员变量,这样就可以对上述功能逻辑进行分层了,服务器类代码如下:
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdio>
#include <unistd.h>
using namespace std;
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
class UdpServer{
public:
UdpServer(const uint16_t &port = defaultport,
const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip)
{}
void Init()
{
// 1. 创建udp socket
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
printf("socket create success, sockfd: %d\n", sockfd_);
// 2. bind socket
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
local.sin_addr.s_addr = inet_addr(ip_.c_str());
bind(sockfd_, (const struct sockaddr *)&local, sizeof(local));
printf("bind success, errno: %d, \
err string: %s\n", errno, strerror(errno));
}
void Run() // 对代码进行分层
{
char inbuffer[size];
while(true)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
//服务器先接收消息
ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1,
0, (struct sockaddr*)&client, &len);
cout<<"客户端说:"<<inbuffer<<endl;
inbuffer[n] = 0;
std::string info = inbuffer;
std::string echo_string = "服务器的回答:"+info;
//再反馈消息
sendto(sockfd_, echo_string.c_str(), echo_string.size()
, 0, (const sockaddr*)&client, len);
}
}
~UdpServer()
{
if(sockfd_>0) close(sockfd_);//关闭描述符
}
private:
int sockfd_; // 网路文件描述符
std::string ip_; // ip地址
uint16_t port_; // 服务器端口号
};
该服务器的逻辑是先接收客户端发送的消息,然后利用recvfrom函数保存客户端的地址信息,再使用sendto函数对客户端进行信息的反馈。
2、实现客户端的逻辑
客户端逻辑和服务器逻辑几乎一样,第一步必须调用socket创建网络描述符,但是第二步客户端不需要进行bind绑定,因为服务器之所以需要绑定是因为服务器必须手动自定义一个端口号,目的就是要让该端口号可见,以便让客户端知道该端口号,这样客户端才能通过该端口号定位服务器。而客户端不需要自定义端口号,因为客户端的主要任务是给服务器发送信息,这个过程服务器是不需要知道客户端的端口号也可以接收客户端的信息,因此客户端的端口号只需要保证其唯一性即可,即交给操作系统来生成,当首次发送数据的时候操作系统就会为客户端生成端口号。
2.1 客户端代码
客户端代码如下:
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
//从命令行参数拿到ip地址和端口号
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport); //转换网络字节序
server.sin_addr.s_addr = inet_addr(serverip.c_str());//转换网络字节序
// 1. socket拿到网络描述符
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
socklen_t len = sizeof(server);
string message;
char buffer[1024];
while (true)
{
cout << "Please Enter@ ";
getline(cin, message);
//2. 向服务器发送信息
sendto(sockfd, message.c_str(), message.size(),
0, (struct sockaddr *)&server, len);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
//3. 打印来自服务器的信息
recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
cout << buffer << endl;
}
close(sockfd);//关闭文件描述符
return 0;
}
3、实现通信
实现通信的前提是让服务器以进程的形式跑起来,然后再让客户端也以进程的形式跑起来,因为网络通信的本质就是进程间通信,而上述代码中客户端本身就是在main函数中执行的,所以此时客户端可以直接运行,但是服务器还只是个类,因此现在只需要用服务器类实现一个main函数,即可完成两个进程的运行。
服务器进程代码如下:
#include "UDPser.hpp"
#include <memory>
#include <cstdio>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
// ./udpserver port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
//从命令行参数拿到端口号
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> svr(new UdpServer(port));
svr->Init();
svr->Run();
return 0;
}
运行结果:
从结果可以看到目前可以正常的进行客户端与服务器之间的通信。
结语
以上就是关于实现UDP网络通信的讲解,实现UDP的核心在于对套接字的理解以及相关接口的逻辑使用,其实只需要记住只要涉及到网络通信,那么socket和bind函数是必须在最开始就调用的。
最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!