跳到主要内容 Linux Socket 编程:UDP 套接字通信实战 | 极客日志
C++
Linux Socket 编程:UDP 套接字通信实战 介绍 Linux 下基于 UDP 套接字的编程实践。首先讲解 socket、bind、sendto、recvfrom 等核心接口及 IP/端口转换函数。随后通过三个实例逐步深入:实现简单的回显通信、构建字典翻译服务(含回调解耦)、开发支持多客户端的聊天室(引入线程池)。文中包含完整的 C++ 服务端与客户端代码示例,并对比了 inet_ntoa 与 inet_ntop 的区别,最后展示了跨平台(Windows/Linux)通信的可能性。
星河入梦 发布于 2026/3/23 更新于 2026/4/16 27K 浏览在之前的网络基础当中我们已经了解了网络基本的概念,了解了计算机当中网络基本的体系结构,并且了解了网络传输的基本流程,学习了在 Socket 基础的知识。那么接下来在本篇当中我们将来具体的学习 Socket 编程当中的 UDP 套接字编程。在此将会使用 Socket 当中提供的接口来实现客户端和服务器之间的通信,本篇当中将会实现字典翻译和简单聊天室两个基于 UDP 套接字实现的具体实例,接下来就开始本篇的学习吧。
1. Socket 通信接口
在使用 UDP 来实现服务器和客户端之间的通信之前,先来详细的了解 bind 等 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), AF_UNIX (本地通信)。
type:套接字类型。常见的有:SOCK_STREAM (有连接的 TCP), SOCK_DGRAM (无连接 UDP)。
protocol:指定协议,正常填 0 即可,系统会自动选择合适的协议。
返回值:成功返回套接字的文件描述符,失败返回 -1。
进行套接字的创建之后就需要使用到 bind 来进行 IP 和端口号的绑定。
绑定端口
#include <sys/types.h>
#include <sys/socket.h>
int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
参数说明:
sockfd:套接字文件描述符,即 socket 的返回值。
addr:一个 addr 结构体的指针,在该结构体当中存储对应的 IP 和端口。
addrlen:addr 结构体大小。
返回值:当绑定成功时返回 0,否则返回 -1。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
通过以上就可以看出使用 bind 来进行绑定的,其中函数的参数包括之前使用 socket 创建出来的套接字文件描述符,除此之外还需要传入一个 sockaddr 的结构体指针。
在此我们需要使用 UDP 来进行客户端和服务器之间的连接通信,那么就是进行网络通信,那么在此就需要传一个 sockaddr_in 的结构体,并且在传入的时候要强制类型转换为 sockaddr。
通过之前的学习我们知道 sockaddr_in 内有以下的成员变量:
在创建结构体之后只需要将其对象内部的成员变量进行对应的赋值即可。
在进行绑定之后由于 UDP 是无连接的,那么这时候就可以直接进行数据的传输和接收了,那么接下来就来了解提供进行数据传输和接收的接口。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom (int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) ;
ssize_t sendto (int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) ;
sockfd:使用 socket 创建的 sockfd 套接字。
buf:接收数据的缓冲区 / 需要发送的数据所在的缓冲区。
len:接收数据的缓冲区大小 / 发送数据缓冲区的大小。
flags:阻塞式 IO 标志位。
src_addr / dest_addr:保存发送方指针 / 接收方的指针。
addrlen:发送方指针大小 / 接收方指针的大小。
返回值:返回发送成功的数据大小,发送失败时返回 -1;返回值大于等于 0 表示发送数据的大小,为 -1 时表示数据发送失败。
2. 基于 UDP 套接字实现简单通信 在以上我们已经了解了基于 socket 套接字进行 UDP 通信需要使用到的接口,那么接下来就试着来使用以上的接口实现一个客户端和服务器之间的简单通信。实现的效果是客户端向服务器发送对应的数据之后服务器将该数据进行处理接下来再发回给客户端。
在此使用三个文件来实现,分别是在 Server.hpp 内实现服务器通信调用的 Server 类;在 Server.cc 当中实现 Server 对象的创建,并且调用内部对应的函数;在 Client 当中实现客户端接发数据的功能。
Server.hpp 首先先将 Server.hpp 当中 Server 类基本的框架实现
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include "log.hpp"
const int defaultfd = -1 ;
class UdpServer {
public :
private :
};
以上实现的框架就先将之前实现的日志文件头文件添加,在 Server 类当中主要实现两个函数,其中一个是进行 socket 创建的以及 bind 绑定的 Init,另外一个是进行数据发送和数据接收的 Start 函数。
在 Server 当中的成员变量就需要有要进行绑定的端口号以及 IP 地址,并且还需要使用一个标志位来标识当前的进程是否处于运行的状态。
int _sockfd;
uint16_t _port;
std::string _ip;
bool _isrunning;
UdpServer (u_int16_t port, std::string &ip) : _sockfd(defaultfd), _port(port), _isrunning(false ), _ip(ip) { }
那么接下来就先来实现 Init 的函数,实现的步骤就是先使用 socket 来创建出对应的套接字,接下来再使用 bind 进行对应端口和 IP 地址的绑定,并且在使用接口的过程当中当出现错误的使用打印出对应的报错日志。
void Init () {
_sockfd = socket (AF_INET, SOCK_DGRAM, 0 );
if (_sockfd < 0 ) {
LOG (LogLevel::FATAL) << "socket error!" ;
exit (1 );
}
LOG (LogLevel::INFO) << "socket success, sockfd:" << _sockfd;
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 ());
int n = bind (_sockfd, (struct sockaddr *)&local, sizeof (local));
if (n < 0 ) {
LOG (LogLevel::FATAL) << "bind error" ;
exit (2 );
}
LOG (LogLevel::INFO) << "bind success, sockfd:" << _sockfd;
}
以上就是实现的 Init 函数,首先使用的就是 socket 来创建出基于 IPv4 且使用 UDP 进行通信的套接字,接下来再创建出一个 sockaddr_in 结构体的对象之后使用 bzero 将结构体进行清零初始化。
#include <strings.h>
void bzero (void *s, size_t n) ;
在此以上构造函数当中传入的套接字和 IP 地址其实都是主机系列的,但是实际上网络当中进行传输的时候使用的是网络系列,那么在此就需要将对应的主机系列转换为对应的网络系列之后才能赋值给 local 对象当中。
#include <arpa/inet.h>
uint16_t htons (uint16_t hostshort) ;
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr (const char *cp) ;
以上在使用对应的接口将主机序列的 IP 和端口号转换为网络序列的 IP 地址和端口号之后接下来就可以将创建出来的 local 对象以及以上创建的 sockfd 套接字作为 bind 函数的参数。并且对 bind 绑定之后的返回值进行判断,在此判断判定的过程是否成功的进行。
在以上的 Init 函数当中创建了套接字之后再进行对应的绑定,那么继续在以下的 Start 函数当中实现客户端和服务器之间的数据通信。
void Start () {
_isrunning = true ;
while (_isrunning) {
char buffer[1024 ];
struct sockaddr_in peer;
socklen_t len = sizeof (peer);
ssize_t s = recvfrom (_sockfd, buffer, sizeof (buffer) - 1 , 0 , (struct sockaddr *)&peer, &len);
if (s > 0 ) {
int peer_port = ntohs (peer.sin_port);
std::string peer_ip = inet_ntoa (peer.sin_addr);
buffer[s] = 0 ;
sendto (_sockfd, buffer, sizeof (buffer)-1 , 0 , (struct sockaddr *)&peer, len);
}
}
}
#include <arpa/inet.h>
uint16_t ntohs (uint16_t netshort) ;
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa (struct in_addr in) ;
Server.cc 以上我们在实现了 Server.hpp 当中的代码之后接下来就可以试着继续将 Server.cc 当中的代码实现,在该文件当中需要实现的是将用户输入的 IP 地址和端口号传给实例化出来的 Server 对象。
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
int main (int argc,char * argv[]) {
if (argc!=3 ) {
std::cerr<<"Usage:" <<argv[0 ]<<" server_ip port" <<std::endl;
return 1 ;
}
uint16_t port=std::stoi (argv[2 ]);
std::string ip=argv[1 ];
std::unique_ptr<UdpServer> usvr=std::make_unique <UdpServer>(port,ip);
usvr->Init ();
usvr->Start ();
return 0 ;
}
以上就将对应的用户输入的参数传到实例化出来的 UdpServer 对象当中,接下来再依次的调用 usvr 当中的 Init 和 Start 函数。
Client.cc 以上我们已经将服务器的 Server.hpp 和 Server.cc 文件的代码实现,那么接下来就需要来实现客户端的代码。
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
int main (int argc, char *argv[]) {
if (argc != 3 ) {
std::cerr << "Usage:" << argv[0 ] << " server_ip server_port" << std::endl;
return 1 ;
}
std::string server_ip = argv[1 ];
uint16_t server_port = std::stoi (argv[2 ]);
int sockfd = socket (AF_INET, SOCK_DGRAM, 0 );
if (sockfd < 0 ) {
std::cerr << "socket error" << std::endl;
return 2 ;
}
struct sockaddr_in server;
memset (&server, 0 , sizeof (server));
server.sin_family = AF_INET;
server.sin_port = htons (server_port);
server.sin_addr.s_addr = inet_addr (server_ip.c_str ());
while (1 ) {
std::string input;
std::cout << "Please input:" ;
std::getline (std::cin, input);
int n = sendto (sockfd, input.c_str (), input.size (), 0 , (struct sockaddr *)&server, sizeof (server));
(void )n;
char buffer[1024 ];
struct sockaddr_in peer;
socklen_t len = sizeof (peer);
int m = recvfrom (sockfd, buffer, sizeof (buffer) - 1 , 0 , (struct sockaddr *)&peer, &len);
if (m > 0 ) {
buffer[m] = 0 ;
std::cout << buffer << std::endl;
}
}
return 0 ;
}
以上大体的代码逻辑就是先通过用户从命令行当中输入的参数得到对应的要连接的服务器的 IP 地址和端口号,接下来再进行套接字的创建,接着创建对应的 sockaddr_in 结构体对象,在此需要注意的是在客户端当中一般是不需要用户显示的进行绑定而是让操作系统来完成对应的工作。
实际上客户端只需要进行将数据传输给服务器即可,进行 bind 的工作交给操作系统来完成,操作系统会根据当前主机的 IP 分配对应的端口号给当前进行传输的进程,这样就可以避免当前的客户端和其他的客户端出现端口号冲突的问题。总的来说就是内核自动帮你完成了'本地 IP + 临时端口'的绑定。
代码测试以及调整 以上我们已经实现了对应的客户端和服务器的代码,那么接下来就继续测试以上实现的代码是否能实现客户端和服务器之间的通信。不过在进行测试的时候先要来了解一个指令——ifconfig。
以上就发现存在两个 IP,这两个 IP 实际上第一个是该主机在局域网当中的 IP,而第二个 IP 是主机的内外环回地址,其作用是能让主机能和自己通信。那么在此你可能就会又疑惑了,为什么在我们的云服务器当中是没有存在对应的公网 IP 的,就是之前连接云服务器使用到的 IP,其实一般来说云服务器的厂商都是不会将公网 IP 配置到云服务器当中的,而是通过 NAT(网络地址转换) 或 虚拟网关转发 的方式进行映射,这样做的目的是保证公网的安全。
接下来就试着先使用本地环回地址来进行 Server 和 Client 之间的通信:
通过测试就可以看出确实能实现两个进程之间的通信,但是目前的问题是需要客户端向服务器传入的 IP 是一样的才能进行通信,例如以下服务器使用内网 IP,而客户端使用内网环回地址就无法实现通信。
这就说明当我们显示的绑定服务器的 IP 之后,客户端在进行连接的时候就只能使用到服务器 bind 的 IP,但是正常来说服务器正常情况下可能存在多个 IP 地址,因此实际上是不能将客户端的 IP 进行显示的绑定的。
在此使用到 INADDR_ANY 这个宏,本质上就是帮服务器绑定的 IP 可以是任意的值。
这时修改之后 Server.hpp 的代码如下所示:
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <functional>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
using namespace LogModule;
using func_t = std::function<std::string (const std::string &)>;
const int defaultfd = -1 ;
class UdpServer {
public :
UdpServer (u_int16_t port) : _sockfd(defaultfd), _port(port), _isrunning(false ) { }
void Init () {
_sockfd = socket (AF_INET, SOCK_DGRAM, 0 );
if (_sockfd < 0 ) {
LOG (LogLevel::FATAL) << "socket error!" ;
exit (1 );
}
LOG (LogLevel::INFO) << "socket success, sockfd:" << _sockfd;
struct sockaddr_in local;
bzero (&local, sizeof (local));
local.sin_family = AF_INET;
local.sin_port = htons (_port);
local.sin_addr.s_addr = INADDR_ANY;
int n = bind (_sockfd, (struct sockaddr *)&local, sizeof (local));
if (n < 0 ) {
LOG (LogLevel::FATAL) << "bind error" ;
exit (2 );
}
LOG (LogLevel::INFO) << "bind success, sockfd:" << _sockfd;
}
void Start () {
_isrunning = true ;
while (_isrunning) {
char buffer[1024 ];
struct sockaddr_in peer;
socklen_t len = sizeof (peer);
ssize_t s = recvfrom (_sockfd, buffer, sizeof (buffer) - 1 , 0 , (struct sockaddr *)&peer, &len);
if (s > 0 ) {
int peer_port = ntohs (peer.sin_port);
std::string peer_ip = inet_ntoa (peer.sin_addr);
buffer[s] = 0 ;
sendto (_sockfd, buffer, sizeof (buffer)-1 , 0 , (struct sockaddr *)&peer, len);
}
}
}
~UdpServer () { }
private :
int _sockfd;
uint16_t _port;
bool _isrunning;
};
接下来再客户端和服务器使用不同 IP 的就可以进行通信了:
以上我们使用内网 IP 或者是本地环回都能进行对应的 bind 了,但是当前使用公网 IP 还是无法进行 bind。那么解决的方法是什么呢?
实际上这就需要我们再云服务器当中进行开放端口的设置
在以上进行了云服务器当中的开放端口操作之后就可以实现在我们实现的服务器当中进行公网 IP 的 bind。
以上就实现了简单的基于 socket 套接字的 UDP 通信,实现客户端和服务器之间的数据交换。
以上实现的数据返回的功能是在 Server 类当中的 Start 函数当中实现的,这时通信和数据的处理是在一个函数当中实现的就显得较为冗余,因此当中就可以使用回调函数 的方式来实现解耦。
void Start () {
_isrunning = true ;
while (_isrunning) {
char buffer[1024 ];
struct sockaddr_in peer;
socklen_t len = sizeof (peer);
ssize_t s = recvfrom (_sockfd, buffer, sizeof (buffer) - 1 , 0 , (struct sockaddr *)&peer, &len);
if (s > 0 ) {
int peer_port = ntohs (peer.sin_port);
std::string peer_ip = inet_ntoa (peer.sin_addr);
buffer[s] = 0 ;
std::string result = _func(buffer);
sendto (_sockfd, result.c_str (), result.size (), 0 , (struct sockaddr *)&peer, len);
}
}
}
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
std::string defaultHandler (const std::string& message) {
std::string ret="Client say:" ;
ret+=message;
return ret;
}
int main (int argc,char * argv[]) {
if (argc!=2 ) {
std::cerr<<"Usage:" <<argv[0 ]<<" port" <<std::endl;
return 1 ;
}
uint16_t port=std::stoi (argv[1 ]);
std::unique_ptr<UdpServer> usvr=std::make_unique <UdpServer>(port,defaultHandler);
usvr->Init ();
usvr->Start ();
return 0 ;
}
3. 基于 UDP 套接字实现字典翻译功能 以上实现的是基于 socket 套接字简单的 UDP 通信,那么接下来基于以上的代码来实现一个在客户端当中用户输入指定的英文单词之后即可得到对应的中文翻译,那么此时服务器要实现的就是得到用户输入的英文单词之后再查询对应中英翻译文件,再将对应的中文翻译返回给客户端。
InetAddr.hpp 在此依旧使用通信和数据处理解耦的方式实现,在 Dict.hpp 当中实现单词翻译的功能,在 Server.hpp 当中实现数据的接发。在 dictionary.txt 当中实现 英文:中文 的键值对,文件当中储存对应的翻译。
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
spring: 春天
autumn: 秋天
father: 父亲
mother: 母亲
brother: 兄弟
sister: 姐妹
water: 水
fire: 火
earth: 地球
sky: 天空
sun: 太阳
moon: 月亮
star: 星星
food: 食物
house: 房子
door: 门
window: 窗户
chair: 椅子
在之前创建对应的 sockaddr_in 对象的时候都是通过调用对应的 htons 等函数来实现,那么这样在需要创建较多的对象代码当中就会显得较为繁琐,因此在此实现一个 InetAddr 进行网络序列和主机序列套接字的转换类。这样就只需要将对应的参数传给类当中的函数即可实现序列的转换。
#pragma once
#include <iostream>
#include <string>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
class InetAddr {
public :
InetAddr (struct sockaddr_in &addr) : _addr(addr) { _port = ntohs (_addr.sin_port); _ip = inet_ntoa (_addr.sin_addr); }
uint16_t Port () { return _port; }
std::string IP () { return _ip; }
private :
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
Dict.hpp 实现了以上的代码之后接下来就可以来实现进行翻译功能 Dict.hpp 文件的代码,在该文件当中需要实现一个 Dict 类,在该类的内部主要实现两个函数来提供给外部进行调用,其中一个是将对应的 Dictionary 文件打开,并且在类当中需要创建一个对应的哈希表来将 Dictionary.txt 文件当中的英文单词向对应的中文翻译形成对应的键值对存储到哈希表当中。同时在创建对应的键值对时形成相应的日志来表明键值对的创建是否出现问题,如果出现问题形成对应的日志信息。
那么接下来就先将 Dict 类当中大体代码结构实现:
#pragma once
#include <iostream>
#include <fstream>
#include <unordered_map>
#include "log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
const std::string defaultdict="./dictionary.txt" ;
const std::string sep=":" ;
class Dict {
public :
Dict (const std::string & path=defaultdict) :_dict_path(path) { }
~Dict () { }
bool LoadDict () {
std::string Translate (const std::string &word,InetAddr& peer) {
private :
std::string _dict_path;
std::unordered_map<std::string,std::string> _dict;
};
在此在 LoadDict 当中需要将 Dictionary.txt 文件打开,那么这时候就需要进行文件的操作,在此时可以选择使用 open 等系统调用或者时 C 当中提供的对应的 fopen 的函数,但是在此使用 C++17 当中提供的 fstream 对应的文件操作接口能更简便的实现。
bool LoadDict () {
std::ifstream in (_dict_path) ;
if (!in.is_open ()) {
LOG (LogLevel::DEBUG)<<"打开字典:" <<_dict_path<<"错误" ;
return false ;
}
std::string line;
while (getline (in,line)) {
auto pos=line.find (sep);
if (pos==std::string::npos) {
LOG (LogLevel::WARNING)<<"解析:" <<line<<"失败" ;
continue ;
}
std::string english=line.substr (0 ,pos);
std::string chinese=line.substr (pos+sep.size ());
if (english.empty () || chinese.empty ()) {
LOG (LogLevel::WARNING)<<"没有有效内容:" <<line;
continue ;
}
_dict.insert ({english,chinese});
LOG (LogLevel::DEBUG)<<"加载:" <<line;
}
in.close ();
return true ;
}
接下来实现了以上创建出对应打开指定文件再哈希表的函数之后,那么接下来再来实现将英文当中翻译为中文的 Translate 函数。
std::string Translate (const std::string &word,InetAddr& peer) {
auto find=_dict.find (word);
if (find==_dict.end ()) {
LOG (LogLevel::INFO)<<"用户:" <<peer.IP ()<<",端口:" <<peer.Port ()<<"翻译" <<"[" <<word<<":" <<"None]" );
return "None" ;
}
LOG (LogLevel::INFO)<<"用户:" <<peer.IP ()<<",端口:" <<peer.Port ()<<"翻译" <<"[" <<find->first<<":" <<find->second<<"]" ;
return find->second;
}
以上我们在实现了对应的翻译功能的文件之后就可以接着来改造之前实现的 Server.hpp、Server.cc 和 Client.cc 文件了。
Server.hpp #pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <functional>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
using func_t = std::function<std::string (const std::string &,InetAddr peer)>;
const int defaultfd = -1 ;
class UdpServer {
public :
UdpServer (u_int16_t port, func_t func) : _sockfd(defaultfd), _port(port), _isrunning(false ), _func(func) { }
void Init () {
_sockfd = socket (AF_INET, SOCK_DGRAM, 0 );
if (_sockfd < 0 ) {
LOG (LogLevel::FATAL) << "socket error!" ;
exit (1 );
}
LOG (LogLevel::INFO) << "socket success, sockfd:" << _sockfd;
struct sockaddr_in local;
bzero (&local, sizeof (local));
local.sin_family = AF_INET;
local.sin_port = htons (_port);
local.sin_addr.s_addr = INADDR_ANY;
int n = bind (_sockfd, (struct sockaddr *)&local, sizeof (local));
if (n < 0 ) {
LOG (LogLevel::FATAL) << "bind error" ;
exit (2 );
}
LOG (LogLevel::INFO) << "bind success, sockfd:" << _sockfd;
}
void Start () {
_isrunning = true ;
while (_isrunning) {
char buffer[1024 ];
struct sockaddr_in peer;
socklen_t len = sizeof (peer);
ssize_t s = recvfrom (_sockfd, buffer, sizeof (buffer) - 1 , 0 , (struct sockaddr *)&peer, &len);
if (s > 0 ) {
int peer_port = ntohs (peer.sin_port);
std::string peer_ip = inet_ntoa (peer.sin_addr);
buffer[s] = 0 ;
InetAddr inet (peer) ;
std::string result = _func(buffer,peer);
sendto (_sockfd, result.c_str (), result.size (), 0 , (struct sockaddr *)&peer, len);
}
}
}
~UdpServer () { }
private :
int _sockfd;
uint16_t _port;
bool _isrunning;
func_t _func;
};
Server.cc #include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include "Dict.hpp"
std::string defaultHandler (const std::string& message) {
std::string ret="Client say:" ;
ret+=message;
return ret;
}
int main (int argc,char * argv[]) {
if (argc!=2 ) {
std::cerr<<"Usage:" <<argv[0 ]<<" port" <<std::endl;
return 1 ;
}
Dict dict;
dict.LoadDict ();
uint16_t port=std::stoi (argv[1 ]);
std::unique_ptr<UdpServer> usvr=std::make_unique <UdpServer>(port,[&dict](const std::string message,InetAddr peer)->std::string{
return dict.Translate (message,peer);
});
usvr->Init ();
usvr->Start ();
return 0 ;
}
Client.cc #include <iostream>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
int main (int argc, char *argv[]) {
if (argc != 3 ) {
std::cerr << "Usage:" << argv[0 ] << " server_ip server_port" << std::endl;
return 1 ;
}
std::string server_ip = argv[1 ];
uint16_t server_port = std::stoi (argv[2 ]);
int sockfd = socket (AF_INET, SOCK_DGRAM, 0 );
if (sockfd < 0 ) {
std::cerr << "socket error" << std::endl;
return 2 ;
}
struct sockaddr_in server;
memset (&server, 0 , sizeof (server));
server.sin_family = AF_INET;
server.sin_port = htons (server_port);
server.sin_addr.s_addr = inet_addr (server_ip.c_str ());
while (1 ) {
std::string input;
std::cout << "Please input:" ;
std::getline (std::cin, input);
int n = sendto (sockfd, input.c_str (), input.size (), 0 , (struct sockaddr *)&server, sizeof (server));
(void )n;
char buffer[1024 ];
struct sockaddr_in peer;
socklen_t len = sizeof (peer);
int m = recvfrom (sockfd, buffer, sizeof (buffer) - 1 , 0 , (struct sockaddr *)&peer, &len);
if (m > 0 ) {
buffer[m] = 0 ;
std::cout << buffer << std::endl;
}
}
return 0 ;
}
以上本质上整体实现的逻辑就是在服务器当中获取到回调方法进行调用,接着将得到的字符串传给对应的函数,接下来通过该函数得到对应英文的翻译。
通过以上的输出结果就可以看到我们以上实现的代码是没有问题的。
4. 基于 UDP 套接字实现简单聊天室 以上我们实现的 UDP 套接字实现字典翻译的功能以及最开始的简单的通信,那么这时是可以实现了服务器和客户端之间的通信,那么这时是否能使用 UDP 来实现一个多个客户端之间的通信功能也就是实现出不同用户之间进行通信的版本。
在此大体实现的逻辑就是当客户端将对应的消息传输给服务器之后,服务器会将该消息转发给当前其他的和服务器进行数据传输的用户。
那么接下来实现的过程还是基于以上的 Server.hpp 来实现通信,让具体的任务通过回调函数的方式到具体的环节当中进行消息转发的功能。在此创建一个 Route.hpp 的文件来实现消息转发的功能。
在实现 Route.hpp 文件的代码之前先将以上实现 InetAddr.hpp 的文件代码再进行修改补全,之前我们只是实现网络序列到主机序列的转换,那么接下来就将主机序列到网络序列转换的函数也实现。
InetAddr.hpp #pragma once
#include <iostream>
#include <string>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <cstring>
class InetAddr {
public :
InetAddr (struct sockaddr_in &addr) : _addr(addr) { _port = ntohs (_addr.sin_port); _ip = inet_ntop (AF_INET, &addr.sin_addr, buffer, sizeof (buffer)); }
InetAddr (const std::string ip, int port) : _ip(ip), _port(port) { memset (&_addr, 0 , sizeof (_addr)); _addr.sin_family = AF_INET; _addr.sin_port = htons (_port); inet_pton (AF_INET, _ip.c_str (), &_addr.sin_addr); }
uint16_t Port () { return _port; }
std::string StringAddr () { return _ip + ":" + std::to_string (_port); }
const struct sockaddr_in &GetAddr () { return _addr; }
bool operator ==(InetAddr &peer) { return _ip == peer._ip && _port == peer._port; }
private :
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
以上当到了 inet_ntop 和 inet_pton 来实现网络序列和网络序列之间 IP 的转换,函数的使用如下所示:
#include <arpa/inet.h>
const char *inet_ntop (int af, const void *src,char *dst, socklen_t size) ;
int inet_pton (int af, const char *src, void *dst) ;
但是当前的问题就来了以上将网络序列准转换为主机序列使用到的时 inet_ntoa,而之前使用到的是 inet_ntop,这两个函数都能使用对应的转换,那么这两个函数实现的原理上有什么样的区别呢?
实际上在 inet_ntoa 使用到的缓冲区是静态的缓冲区,那么这就使得在使用该函数的过程当中只有一份的缓冲区可以使用,这就会使得在使用多个 inet_ntohs 的时候会出现覆盖的问题。
例如以下的代码:
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main () {
struct in_addr a1,a2;
inet_pton (AF_INET,"192.168.0.1" ,&a1);
inet_pton (AF_INET,"0.0.0.0" ,&a2);
const char * p1=inet_ntoa (a1);
const char * p2=inet_ntoa (a2);
printf ("IP1:%s\n" ,p1);
printf ("IP2:%s\n" ,p2);
return 0 ;
}
以上的代码当中就是先创建出两个 in_addr 结构体对象,接下来再使用 inet_pton 将对应的主机序列 IP 转换为网络序列,接下来使用 inet_ntoa 将对应网络序列转换为主机序列并得到对应的字符串指针,那么这时将两个指针指向的内容进行打印就会发现两个组织指向的地址是一样的。
那么这时就可以看出确实在缓冲区当中的数据是被覆盖的,这时输出的结果就都是 0.0.0.0
而使用到 inet_ntop 的时候就不会出现以上的问题了,因为该函数的缓冲区是使用用户创建的,那么这时就不会再出现覆盖的问题。
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main () {
struct in_addr a1,a2;
inet_pton (AF_INET,"192.168.0.1" ,&a1);
inet_pton (AF_INET,"0.0.0.0" ,&a2);
char buffer1[1024 ],buffer2[1024 ];
const char * p1=inet_ntop (AF_INET,&a1,buffer1,sizeof (buffer1));
const char * p2=inet_ntop (AF_INET,&a2,buffer2,sizeof (buffer2));
printf ("IP1:%s\n" ,p1);
printf ("IP2:%s\n" ,p2);
return 0 ;
}
以上就可以将两个 IP 都正常的输出而不会出现覆盖的问题。
以上将 InetAddr.hpp 的代码进行修改之后接下来就来将对应实现将一个用户的所有的消息发送给当前所有的用户的功能实现,在此在 Route.hpp 当中实现,大致需要实现对的功能就包括两个方面:1.判断当前的用户是否之前是否已经进行数据的传输,若没有将用户的数据添加到对应的哈希表当中 2.将从客户端当中得到的数据转发给其他的客户端当中
了解以上的要求之后接下来就来试着实现文件当中的代码:
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "log.hpp"
#include "InetAddr.hpp"
#include "Mutex.hpp"
using namespace LogModule;
using namespace MutexNamespace;
class Route {
private :
bool IsExit (InetAddr &peer) {
for (auto &x : _online_user) {
if (x == peer) return true ;
}
return false ;
}
void AddUser (InetAddr &peer) {
LOG (LogLevel::INFO) << "新增加一个在线用户" << peer.StringAddr ();
_online_user.push_back (peer);
}
void EraseUser (InetAddr &peer) {
auto it = _online_user.begin ();
while (it != _online_user.end ()) {
if (*it == peer) {
LOG (LogLevel::INFO) << "删除一个在线用户" << peer.StringAddr () << "成功" ;
_online_user.erase (it);
break ;
}
it++;
}
}
public :
Route () { }
~Route () { }
void MessageRoute (int sockfd, const std::string &Message, InetAddr &peer) {
LockGuard lock (_mutex) ;
if (!IsExit (peer)) {
AddUser (peer);
}
std::string send_message = peer.StringAddr () + "#" + Message;
for (auto &x : _online_user) {
sendto (sockfd, send_message.c_str (), send_message.size (), 0 , (const struct sockaddr *)&x.GetAddr (), sizeof (x.GetAddr ()));
}
if (Message == "QUIT" ) {
LOG (LogLevel::INFO) << "删除一个在线用户" << peer.StringAddr ();
EraseUser (peer);
}
}
private :
std::vector<InetAddr> _online_user;
Mutex _mutex;
};
Server.hpp 以上就实现了对应进行消息转发的函数 MessageRoute,那么这时在 Server.hpp 当中就可以通过回调函数的方式来实现。该函数的参数有三个分别是对应的套接字、消息字符串、和进行消息发送的 InetAddr 对象。
接下来就将之前实现的 Server.hpp 在进行修改,不过修改的内容其实不多,只需要在 Start 当中做出轻微的调整即可。
以下只将该文件当中相比之前实现不同的内容写出来,其他的不做修改:
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <functional>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
using func_t = std::function<void (int sockfd,const std::string &,InetAddr peer)>;
const int defaultfd = -1 ;
class UdpServer {
public :
void Start () {
_isrunning = true ;
while (_isrunning) {
char buffer[1024 ];
struct sockaddr_in peer;
socklen_t len = sizeof (peer);
ssize_t s = recvfrom (_sockfd, buffer, sizeof (buffer) - 1 , 0 , (struct sockaddr *)&peer, &len);
if (s > 0 ) {
buffer[s] = 0 ;
InetAddr client (peer) ;
_func(_sockfd,buffer,client);
}
}
}
private :
int _sockfd;
uint16_t _port;
bool _isrunning;
func_t _func;
};
Server.cc 将以上的 Server.hpp 实现之后接下来就继续将 Server.cc 的代码也进行修改,实际上和之前修改 Server.hpp 类似也是只需要一小部分的代码即可。
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"
#include <functional>
using namespace ThreadPoolModule;
using func_t2 =std::function<void ()>;
int main (int argc,char * argv[]) {
if (argc!=2 ) {
std::cerr<<"Usage:" <<argv[0 ]<<" port" <<std::endl;
return 1 ;
}
Route r;
ThreadPool<func_t2>* tp=ThreadPool<func_t2>::GetInstance ();
uint16_t port=std::stoi (argv[1 ]);
std::unique_ptr<UdpServer> usvr=std::make_unique <UdpServer>(port,[&r,&tp](int sockfd,const std::string& message,InetAddr peer){
func_t2 t=std::bind (&Route::MessageRoute,&r,sockfd,message,peer);
tp->Enqueue (t);
});
usvr->Init ();
usvr->Start ();
return 0 ;
}
以上依旧是使用到回调函数的方式来进行,并且在 Server 对象的创建当中第二个参数是使用 lambda 表达式,表达式当中的使用 bind 来进行参数绑定。
Client.cc 以上我们已经将服务器部分的代码实现完毕,那么接下来就只需要来实现 Client.cc 也就是客户端的代码即可。
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include "Thread.hpp"
using namespace ThreadModlue;
int sockfd = 0 ;
pthread_t tid;
std::string server_ip;
uint16_t server_port = 0 ;
void Send () {
struct sockaddr_in server;
memset (&server, 0 , sizeof (server));
server.sin_family = AF_INET;
server.sin_port = htons (server_port);
server.sin_addr.s_addr = inet_addr (server_ip.c_str ());
const std::string online = "inline" ;
sendto (sockfd, online.c_str (), online.size (), 0 , (struct sockaddr *)&server, sizeof (server));
while (1 ) {
std::string input;
std::cout << "Please input:" ;
std::getline (std::cin, input);
int n = sendto (sockfd, input.c_str (), input.size (), 0 , (struct sockaddr *)&server, sizeof (server));
(void )n;
if (input == "QUIT" ) {
pthread_cancel (tid);
break ;
}
}
}
void Receive () {
pthread_detach (pthread_self ());
while (1 ) {
char buffer[1024 ];
struct sockaddr_in peer;
socklen_t len = sizeof (peer);
int m = recvfrom (sockfd, buffer, sizeof (buffer) - 1 , 0 , (struct sockaddr *)&peer, &len);
if (m > 0 ) {
buffer[m] = 0 ;
std::cerr << buffer << std::endl;
}
}
}
int main (int argc, char *argv[]) {
if (argc != 3 ) {
std::cerr << "Usage:" << argv[0 ] << " server_ip server_port" << std::endl;
return 1 ;
}
server_ip = argv[1 ];
server_port = std::stoi (argv[2 ]);
sockfd = socket (AF_INET, SOCK_DGRAM, 0 );
if (sockfd < 0 ) {
std::cerr << "socket error" << std::endl;
return 2 ;
}
Thread send (Send) ;
Thread rev (Receive) ;
send.Start ();
rev.Start ();
tid = send.Tid ();
send.Join ();
return 0 ;
}
以上就将基于 UDP 套接字实现的简单聊天室实现了,实际上大致的原理图如下所示:
本质上我们实现对的聊天室当中的 Server 端的实现就是应该生产消费模型,生产者就是对应的获取消息的 Server 对象,消费者就是线程池当中的线程,交易场所就是线程池当中的任务队列,服务器将得到的消息给线程池当中,接下来线程池就可以进行任务分配的工作。
接下来就将以上的代码进行编译之后形成对应的可执行程序 Server 和 Client。运行程序查看是否能实现客户端和服务器之间的通信。
通过以上的输出可以看出我们实现的代码是能实现基本的通信的,但是目前的问题是用户输入和输出的消息是混杂在一起的,这和我们平时使用的聊天消息分离的正常逻辑是违背的。
实际上我们也可以通过 Qt 等来实现图形化的聊天室界面,但是目前我们还是使用简单的方式来实现即可,在此我们解决的方案是之前在实现的代码当中将客户端当中输出输出到标准错误当中,那么我们只需要将客户端当中的标准错误输出到另外的 shell 当中即可。
先查看/dev/pts 当中的 shell 编号,使用 ls 来进行查看,接下来通过 echo 来判定不同的 shell 的编号。
如上所示就让上方的 shell 来输出服务器发送的消息,使用下方的 shell 来进行获取用户输入的内容。
启动客户端和服务器之后就能实现以下形式的聊天室。
接下来就试着使用不同的服务器进行通信,在此将可执行程序 Client 拷贝到另外应的华为云当中,接下来运行该程序就可以看到在聊天室当中能实现对应的消息。
以上就将基于 UDP 套接字简单聊天室的实现了,完整的代码如下:
实际上除了以上的 Linux 客户端之间对的通信之外还可以使用 UDP 来实现 Linux 客户端和 Windows 之间的通信。
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning (disable : 4996)
#pragma comment(lib, "ws2_32.lib" )
std::string serverip = "1.12.75.231" ;
uint16_t serverport = 8080 ;
int main () {
WSADATA wsd;
WSAStartup (MAKEWORD (2 , 2 ), &wsd);
struct sockaddr_in server;
memset (&server, 0 , sizeof (server));
server.sin_family = AF_INET;
server.sin_port = htons (serverport);
SOCKET sockfd = socket (AF_INET, SOCK_DGRAM, 0 );
if (sockfd == SOCKET_ERROR) {
std::cout << "socker error" << std::endl;
return 1 ;
}
std::string message;
char buffer[1024 ];
while (true ) {
std::cout << "Please Enter@ " ;
std::getline (std::cin, message);
if (message.empty ()) continue ;
sendto (sockfd, message.c_str (), (int )message.size (), 0 ,(struct sockaddr*)&server, sizeof (server));
struct sockaddr_in temp;
int len = sizeof (temp);
int s = recvfrom (sockfd, buffer, 1023 , 0 , (struct sockaddr*)&temp, &len);
if (s > 0 ) {
buffer[s] = 0 ;
std::cout << buffer << std::endl;
}
}
closesocket (sockfd);
WSACleanup ();
return 0 ;
}
在 VS2022 当中运行以上的程序就可以实现 Windows 和 Linux 之间的消息通信。