【计算机网络】UDP网络编程、英汉字典以及多线程聊天室编写
📚 博主的专栏
🐧 Linux | 🖥️ C++ | 📊 数据结构 | 💡C++ 算法 | 🅒 C 语言 | 🌐 计算机网络
上篇文章:计算机网络基础概念
下篇文章:TCP网络编程
文章摘要
本文系统讲解UDP网络编程的实现与优化,涵盖三个版本迭代:V1实现多客户端回显服务器,详解套接字创建、端口绑定及数据收发核心逻辑;V2升级为英译汉字典服务器,通过加载字典文件实现单词翻译功能,解耦网络I/O与业务逻辑;V3引入线程池与在线用户管理,构建多线程聊天室,支持消息广播及客户端动态上下线,并通过锁机制解决资源竞争问题。文章还分析inet_ntoa的线程安全隐患,优化为inet_ntop,并提供完整代码示例与调试技巧,助力读者掌握UDP高并发场景下的开发要点。
目录
UDP 网络编程
V1 版本 - echo server(多客户端发送消息)
简单的回显服务器和服务端代码
1.socket():创建套接字
创建socket文件:
2.bind():绑定套接字到本地地址
第一阶段,创建套接字与绑定完整代码:
3.recvfrom()用于接收 UDP 数据报的系统调用
如何进行通信
4.sendto()
客户端
client的端口号,一般不让用户自己设定,而是让client OS随机选择:
./udp_client server_ip server_port
客户端代码1.0:
注意,在云服务器上,服务端不能直接(也强烈不建议)bind自己的公网ip与内网ip(是虚拟的)
为什么ip设置为0:可以让服务器bind任意ip
优化代码结构:封装网络地址
封装一个InetAddr :
服务端代码:
V2 版本 - DictServer(实现一个英译汉的网络字典)
在udpserver.hpp中
在Start函数当中:服务器只需要读取数据与发送数据
翻译方法:
V3 版本 - 简单聊天室
Route.hpp
UdpServerMain.cc
修改InetAddr.hpp:
重新封装客户端代码:
优化,给收到的消息添加是谁:
inet_ntoa
UDP 网络编程
V1 版本 - echo server(多客户端发送消息)
简单的回显服务器和服务端代码
准备好以下文件:
其中LockGuard.hpp以及Log.hpp(打印日志消息)在我的上一篇博客中有详细讲解以及完整代码
LockGuard.hpp(保证安全输出)
#pragma once#include <pthread.h>class LockGuard { public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);} private:pthread_mutex_t *_mutex; };
Makefile
.PHONY:all all:udpserver udpclient udpserver:UdpServerMain.ccg++ -o $@ $^ -std=c++14 udpclient:UdpClientMain.ccg++ -o $@ $^ -std=c++14 .PHONY:clean clean: rm -rf udpserver udpclient
可以存在多个服务器,但是服务器之间不进行拷贝。
定义一个nocopy类,让服务器类来继承他。
nocopy.hpp
#pragma onceclass nocopy { public:nocopy() {}nocopy(const nocopy &) = delete;const nocopy &operator=(const nocopy &) = delete;~nocopy() {} };
网络套接字编写的时候固定需要使用的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include<sys/types.h>
#include<sys/socket.h>
1.socket()
:创建套接字
功能:创建一个套接字,用于后续的网络通信(相当于打开网卡(网卡也是文件))。
语法:
int socket(int domain, int type, int protocol);
参数:
domain
:协议族,如AF_INET
(IPv4)或AF_INET6
(IPv6)。
type
:套接字类型,如SOCK_STREAM
(TCP)或SOCK_DGRAM
(UDP)。
protocol
:协议,通常设置为0
,由type
决定。
返回值:成功返回套接字描述符(新的文件描述符),失败返回
-1
。
对于Linux文件系统有需要学习的朋友可以看博主的这篇文章文件描述符fd,或者看看Linux专栏,总有你需要的
任何一个UDP服务器,必须有一个sockfd套接字描述符
未来进行收发消息也需要套接字描述符:
这个sockfd是创建套接字的返回值,如果没有打开其他文件,sockfd的值就应该是3
创建socket文件:
void InitServer(){ //AF_INET(网络套接字)、SOCK_DGRAM(用户数据报套接字)、协议字段(0)_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);//表示我使用IPV4来进行网络通信if(_sockfd < 0){//属于致命错误LOG(FATAL, "socket error\n");exit(SOCKET_ERROR);}LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd);//_sockfd = 3}
测试函数:
#include "UdpServer.hpp"
#include<memory>using namespace log_ns;int main()
{EnableScreen();std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>();//C++14//初始化服务器usvr->InitServer();usvr->Start();return 0;
}
运行结果:
由于客户端和服务器都需要有sockfd
2.bind():绑定套接字到本地地址
作为服务器,需要bind绑定,将套接字信息和套接字绑定起来
bind()
:绑定套接字到本地地址
功能:将套接字绑定到本地的 IP 地址和端口号。
语法:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
:由socket()
创建的套接字描述符。
addr
:指向sockaddr
结构的指针,包含本地地址信息。
addrlen
:addr
的长度。返回值:成功返回
0
,失败返回-1
。实际上
sockaddr结构体中
填写的内容就是上篇博客所讲到的
我们填写的内容也就是根据sockaddr_in结构体中的成员:
struct sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of `struct sockaddr'. */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];};
首先我们要填写的内容就是sin_(16位地址类型(4字节),也就是选择要网络通信(ipv4(AF_INET)、ipv6(AF_INET6))还是本地通信)我们一般采用网络通信IPV4:AF_INET
typedef unsigned short int sa_family_t; #define __SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family //这里的##意思就是可以替换
每一个服务器,要有自己的端口号,这个需要我们自己传入,并且需要考虑大小端问题,因此我们需要将主机(host)端口号转成正确的网络(net)字节序,16位(small)(TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节.),使用对应的接口:htons();
填写IP地址:是一个32位,local.sin_addr.s_addr
typedef uint32_t in_addr_t; struct in_addr{in_addr_t s_addr;};
在未来,用户对象的时候一般是这样传入的:
UdpServer user("192.1.1.1", 8880);所传入的ip地址是一个字符串,因此传入后我们还需要对字符串进行处理成4字节IP,成为需要网络序列的IP。
转换方法,利用结构体的特性,在结构体中,定义四个4字节的字符类型,再将,ipaddr的地址强转成我们定义的ip结构体指针,就能让结构体中的四个成员分别指向ipaddr的地址,再转化成4字节:
而我们所讲的转化思路的方法,库已经为我们准备好了,我们只需要调用接口:inet_addr
可以将一个字符串形式的ip地址,转成第一:4个字节,第二:转成网络序列,返回类型就是
in_addr_t inet_addr(const char *cp);
返回类型就是s.addr的返回类型in_addr_t(暂时写法,后面优化)
当前我们已经填充好,整个local信息的结构体,还需要做的是将这些信息设置进内核当中,并且将信息和套接字进行绑定。
正常情况下,服务器是一直开启的,因此设置一个bool值,记录服务器的状态,然后开启服务器时,将_isrunning设置为开启状态:
void Start(){_isrunning = true;while(_isrunning){sleep(1);}}
第一阶段,创建套接字与绑定完整代码:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
// 网络套接字编写的时候需要用到的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>#include "nocopy.hpp"
#include "Log.hpp"using namespace log_ns;static const int gsockfd = -1;
static const u_int16_t glocalport = 8888;
// 使用enum规定一些常量
enum
{SOCKET_ERROR = 1,BIND_ERROR = 2
};//UdpServer user("192.1.1.1", 8880);
class UdpServer : public nocopy
{
public:UdpServer(const std::string &localip, u_int16_t localport = glocalport): _sockfd(gsockfd), _localport(localport), _localip(localip), _isrunning(false){}void InitServer(){// 1.创建socket文件// AF_INET(网络套接字)、SOCK_DGRAM(用户数据报套接字)、协议字段(0)_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // 表示我创建的文件是网络套接字if (_sockfd < 0){// 属于致命错误LOG(FATAL, "socket error\n");exit(SOCKET_ERROR);}LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd); //_sockfd = 3// 2.bindstruct sockaddr_in local; // 包含头文件// 清空结构体,memset或者使用bzeromemset(&local, 0, sizeof(local));// IPV4:表示我使用IPV4来进行网络通信,传入的信息是网络信息,与上面的sockfd传入时要一致local.sin_family = AF_INET;// 端口号,作为一个服务器,得有自己的服务器// 使用对应的接口:htons();将主机序列转成网络序列,16位local.sin_port = htons(_localport);// ip地址local.sin_addr.s_addr = inet_addr(_localip.c_str()); //1、4字节IP 2、需要网络序列的IP// 套接字id 、 指向 sockaddr 结构的指针,包含本地地址信息、local的长度// int n = ::bind(_sockfd, &local, sizeof(local));int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));if(n < 0){LOG(FATAL, "bind error\n");exit(BIND_ERROR);//绑定失败}LOG(DEBUG, "socket bind success\n");}void Start(){_isrunning = true;char inbuffer[1024];while(_isrunning){struct sockaddr_in peer;//远端的结构体socklen_t len = sizeof(peer);//从_sockfd套接字获取信息,获得的信息放在buffer里,长度就是buffer的大小-1,flag就设置为0,ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&peer, &len);if(n > 0){inbuffer[n] = 0;//让服务端打印出收到的客户端消息std::cout << "client say# " << inbuffer << std::endl;//我们要给客户端返回消息std::string echo_string = "[udp_server echo] #";echo_string += inbuffer;//已知客户端在peer中sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);}}}~UdpServer(){if(_sockfd > gsockfd) ::close(_sockfd);}private:int _sockfd; // 因为0,1,2被占用了,因此打印出来的u_int16_t _localport; // 本地端口号,主机序列,16位的数字std::string _localip; // TODO:后面专门要处理ip,用户一般传过来的是字符串bool _isrunning;
};
测试代码:
#include "UdpServer.hpp" #include<memory>using namespace log_ns;int main() {uint16_t port = 8899;std::string ip = "127.0.0.1";//本主机,localhost,本地环回EnableScreen();std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);//C++14//初始化服务器usvr->InitServer();usvr->Start();return 0; }
运行结果:
测试错误ip:
std::string ip = "8.0.0.1";
当udp一旦跑起来了,就可以使用:u表示查看udp、p表示查看更多竞争信息、a表示查看详细信息,n表示number的意思,将能显示成数字的就替代成数字
netstat -upa
netstat -nupa
第二阶段,我们进行如何在死循环中进行通信的讲解
第一步,从网络当中收数据
3.recvfrom()用于接收 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);
参数说明
buf
类型:
void *
描述:指向接收缓冲区的指针。接收的数据将被存储到这个缓冲区中。
len
类型:
size_t
描述:接收缓冲区的大小,单位是字节。这决定了最多可以接收多少字节的数据。
flags
类型:
int
描述:用于控制接收行为的标志。常见标志包括:通常设置为0
0
:默认行为,阻塞直到数据到达。
MSG_PEEK
:查看数据但不从套接字接收队列中移除数据。
MSG_DONTWAIT
:非阻塞模式,如果数据不可用则立即返回。
src_addr
类型:
struct sockaddr *
描述:指向
sockaddr
结构的指针,用于存储发送方的地址信息。如果不需要获取发送方的地址,可以传入NULL
。
addrlen
类型:
socklen_t *
描述:指向
socklen_t
类型的指针,表示src_addr
的大小。在调用前,需要将addrlen
设置为src_addr
的大小。调用后,addrlen
会被更新为实际存储的地址信息的大小。返回值
成功:返回接收到的字节数。
失败:返回
-1
,并设置errno
以指示错误原因。
如何进行通信
void Start(){_isrunning = true;char inbuffer[1024];while(_isrunning){struct sockaddr_in peer;//远端的结构体socklen_t len = sizeof(peer);ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&peer, &len);}}
我们收消息的时候不仅根据套接字描述符得知发的消息是什么,我也要知道这个消息是谁发来的,要知道发消息的人是谁,就要知道客户端的套接字、IP地址以及端口号,因此我们定义了peer,以及peer的长度
4.sendto()
sendto
用于向指定的套接字发送数据。它既可以用于面向连接的套接字(如 TCP),也可以用于无连接的套接字(如 UDP)。对于无连接的套接字,sendto
允许在每次发送时指定目标地址。
#include <sys/types.h>
#include <sys/socket.h>ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
buf
:指向要发送的数据缓冲区的指针。
len
:要发送的数据的长度(以字节为单位)。
flags
:发送标志,通常设置为 0。
dest_addr(对应的客户端)
:目标地址的结构体指针,通常是一个struct sockaddr
类型的指针。对于无连接的套接字(如 UDP),每次发送数据时都需要指定目标地址。
addrlen(客户端长度)
:目标地址结构体的长度。3. 返回值
成功时,返回实际发送的字节数。
失败时,返回 -1,并设置
errno
以指示错误原因
于是我们填写好函数参数:
void Start(){_isrunning = true;char inbuffer[1024];while(_isrunning){struct sockaddr_in peer;//远端的结构体socklen_t len = sizeof(peer);//从_sockfd套接字获取信息,获得的信息放在buffer里,长度就是buffer的大小-1,flag就设置为0,ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&peer, &len);if(n > 0){inbuffer[n] = 0;//我们要给客户端返回消息std::string echo_string = "[udp_server echo] #";echo_string += inbuffer;//已知客户端在peer中sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);}}}
以上就是服务端的内容,接下来我们讲客户端如何与服务器建立通信
客户端
首先,实际情况下,通常都是客户端先访问服务器,因此客户端必须要知道服务器的相关信息:IP地址和端口号。其次客户端也有自己的套接字。
那么客户端 需不需要bind他自己的IP和端口呢(服务器是需要的):
client需要bind他自己的IP和端口,但是不需要显式 bind他自己的ip和端口(不需要像服务端显式填充)。
client的端口号,一般不让用户自己设定,而是让client OS随机选择:
client 在首次向服务器发送数据的时候,OS会自动给client bind他自己的IP和端口
因此客户端只需要创建好自己的套接字,然后收发消息就可以了
首先我们知道不管是客户端还是服务器,都是各自OS上的一个进程
我们手机上有许多客户端(抖音、微信....),这些客户端在启动之后都会变成我的手机上的一个进程,他们的端口号必须是不同的,保证客户端进程的唯一性,因此由操作系统自己随机选择。自己设定有可能会导致端口号相同,出现客户端上的端口号冲突,会导致其中一个进程无法执行。客户端的端口号,只要能保证该客户端进程在该OS的唯一性就可以了。
未来 在命令行对于客户端会以这样的方式来调用、客户端要知道服务端的IP和端口号
./udp_client server_ip server_port
127.0.0.1等同于本主机,用此IP来通信,相当于数据不会走到网络端,把网络协议栈走一圈,直接走到服务端,这样不会走到网络端可以保证(排除)客户端和服务器双方软件的内部不会出错,未来测试通过之后,再引入网络,让其跨网络通信。C(client)S(server)
IP地址任何人都可以访问,强关联的,只读的,端口号一旦设定好,必须是唯一的,学号标识唯一性。
客户端代码1.0:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>// 未来 在命令行对于服务端会以这样的方式来调用、客户端要知道服务端的IP和端口号
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage:" << argv[0] << "server-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 创建客户端套接字int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "create socket error" << std::endl;exit(0);}// 获取服务器对应的套接字信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = serverport;server.sin_addr.s_addr = inet_addr(serverip.c_str());// 客户端先访问服务器// 客户端 需不需要bind他自己的IP和端口呢(服务器是需要的)while (1){std::string line;std::cout << "Please Enter#" << " ";std::getline(std::cin, line);// 给服务器发送消息int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));if (n > 0){// 我们本次用这个temp来当占位符struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];// udp客户端是不面向连接的,一般有多个服务端向客户端发消息,因此客户端需要区分发消息的是谁int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}else{std::cout << "recvfrom error" <<std::endl;break;}}else{std::cout << "sendto error" <<std::endl;break;}}// 关闭套接字::close(sockfd);return 0;
}
运行结果:服务端并没有收到消息
解决问题:
1.udpserver在我们的云服务器上,绑定ip比较特殊,在服务端,也需要将ip和端口通过命令行传入:
因此对于服务端:
#include "UdpServer.hpp" #include<memory>using namespace log_ns;// ./udp_server local-ip local-port int main(int argc, char *argv[]) {if (argc != 3){std::cerr << "Usage:" << argv[0] << "local-ip local-port" << std::endl;exit(0);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);EnableScreen();std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);//C++14//初始化服务器usvr->InitServer();usvr->Start();return 0; }
注意,在云服务器上,服务端不能直接(也强烈不建议)bind自己的公网ip与内网ip(是虚拟的)
真实的通过ifconfig可以看到,内网ip,是能够绑定的,但是不能从外网上收到消息了。
作为云服务器,一般给云服务器的端口号可以自己指定设置,而ip地址设置为0
但是服务器仍然不能收到消息:
这就说明是我的代码的问题
为什么ip设置为0:可以让服务器bind任意ip
不同ip同一端口发送的消息,都能让服务器收到。因此我们写死服务端ip地址,让端口号自行输入
因此修改服务端构造函数,成员变量,以及给local传ip时传INADDR_ANY(服务器端进行任意ip地址绑定也就是0) :
对于main函数:
#include "UdpServer.hpp" #include<memory>using namespace log_ns;// ./udp_server local-port int main(int argc, char *argv[]) {if (argc != 2){std::cerr << "Usage:" << argv[0] << "local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);EnableScreen();std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);//C++14//初始化服务器usvr->InitServer();usvr->Start();return 0; }
服务端收不到消息的原因:
server.sin_port = htons(serverport);
修改问题之后:
如何知道客户端信息:客户端ip以及客户端端口号,更新服务端start函数中的代码
这里就是操作系统随机生成的端口号
支持多个客户端:
优化代码结构:封装网络地址
在未来,我们想根据将套接字信息封装,通过对象来获取其中的内容,包括转为网络字节序
InetAddr addr(peer);addr.Ip();addr.Port();
封装一个InetAddr :
#pragma once
#include <iostream>
#include <string>
#include <cstring>
// 网络套接字编写的时候需要用到的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>class InetAddr
{
private:void ToHost(const struct sockaddr_in &addr){_port = ntohs(addr.sin_port);//这是从网络过来的因此要做处理_ip = inet_ntoa(addr.sin_addr);}public:InetAddr(const struct sockaddr_in &addr):_addr(addr){ToHost(addr);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}~InetAddr(){}
private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};
更新Start代码:
void Start(){_isrunning = true;char inbuffer[1024];while(_isrunning){struct sockaddr_in peer;//远端的结构体socklen_t len = sizeof(peer);//从_sockfd套接字获取信息,获得的信息放在buffer里,长度就是buffer的大小-1,flag就设置为0,ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&peer, &len);if(n > 0){InetAddr addr(peer);inbuffer[n] = 0;//让服务端打印出收到的客户端消息std::cout << "[" << addr.Ip() << ":"<< addr.Port() << "]#" << inbuffer << std::endl;//我们要给客户端返回消息std::string echo_string = "[udp_server echo] #";echo_string += inbuffer;//已知客户端在peer中sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);}else{std::cout << "recvfrom error" << std::endl;}}}
服务端代码:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
// 网络套接字编写的时候需要用到的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>#include "nocopy.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"using namespace log_ns;static const int gsockfd = -1;
static const u_int16_t glocalport = 8888;
// 使用enum规定一些常量
enum
{SOCKET_ERROR = 1,BIND_ERROR = 2
};//UdpServer user("192.1.1.1", 8880);
class UdpServer : public nocopy
{
public:UdpServer(u_int16_t localport = glocalport): _sockfd(gsockfd), _localport(localport), _isrunning(false){}void InitServer(){// 1.创建socket文件// AF_INET(网络套接字)、SOCK_DGRAM(用户数据报套接字)、协议字段(0)_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // 表示我创建的文件是网络套接字if (_sockfd < 0){// 属于致命错误LOG(FATAL, "socket error\n");exit(SOCKET_ERROR);}LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd); //_sockfd = 3// 2.bindstruct sockaddr_in local; // 包含头文件// 清空结构体,memset或者使用bzeromemset(&local, 0, sizeof(local));// IPV4:表示我使用IPV4来进行网络通信,传入的信息是网络信息,与上面的sockfd传入时要一致local.sin_family = AF_INET;// 端口号,作为一个服务器,得有自己的服务器// 使用对应的接口:htons();将主机序列转成网络序列,16位local.sin_port = htons(_localport);// ip地址local.sin_addr.s_addr = INADDR_ANY; //1、4字节IP 2、需要网络序列的IP// 套接字id 、 指向 sockaddr 结构的指针,包含本地地址信息、local的长度// int n = ::bind(_sockfd, &local, sizeof(local));int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));if(n < 0){LOG(FATAL, "bind error\n");exit(BIND_ERROR);//绑定失败}LOG(DEBUG, "socket bind success\n");}void Start(){_isrunning = true;char inbuffer[1024];while(_isrunning){struct sockaddr_in peer;//远端的结构体socklen_t len = sizeof(peer);//从_sockfd套接字获取信息,获得的信息放在buffer里,长度就是buffer的大小-1,flag就设置为0,ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&peer, &len);if(n > 0){InetAddr addr(peer);inbuffer[n] = 0;//让服务端打印出收到的客户端消息std::cout << "[" << addr.Ip() << ":"<< addr.Port() << "]#" << inbuffer << std::endl;//我们要给客户端返回消息std::string echo_string = "[udp_server echo] #";echo_string += inbuffer;//已知客户端在peer中sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);}else{std::cout << "recvfrom error" << std::endl;}}}~UdpServer(){if(_sockfd > gsockfd) ::close(_sockfd);}private:int _sockfd; // 因为0,1,2被占用了,因此打印出来的u_int16_t _localport; // 本地端口号,主机序列,16位的数字bool _isrunning;
};
以上就是V1版本
V2 版本 - DictServer(实现一个英译汉的网络字典)
服务器一般主要是用来进行网络数据读取和写入的。IO的
服务器IO逻辑 和 业务逻辑 解耦
在udpserver.hpp中
首先我们设置一个函数类型:返回值为string,参数为string(因为字典一般是输入一个字符串、输出一个字符串)
using func_t = std::function<std::string (std::string)>;
设定服务名:
func_t _func;
于是在UdpServer的构造函数当中:将要做的服务,传进来
UdpServer(func_t func, u_int16_t localport = glocalport): _func(func), _sockfd(gsockfd), _localport(localport), _isrunning(false){}
在Start函数当中:服务器只需要读取数据与发送数据
服务器根本不知道上层做了什么,约定好,将得到的客户端传来的信息传给上层,再将上层处理好的内容传回给客户端。
void Start(){_isrunning = true;char inbuffer[1024];while (_isrunning){struct sockaddr_in peer; // 远端的结构体socklen_t len = sizeof(peer);// 从_sockfd套接字获取信息,获得的信息放在buffer里,长度就是buffer的大小-1,flag就设置为0,ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){InetAddr addr(peer);inbuffer[n] = 0;// 让服务端打印出收到的客户端消息std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]#" << inbuffer << std::endl;//获取到一个一个的单词,然后处理,处理后得到返回结果std::string result = _func(inbuffer);//传回给客户端sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);}else{std::cout << "recvfrom error" << std::endl;}}}
翻译方法:
字典:dict.txt
apple: 苹果 banana: 香蕉 cat: 猫 dog: 狗 book: 书 pen: 笔 happy: 快乐的 sad: 悲伤的 run: 跑 jump: 跳 teacher: 老师 student: 学生 car: 汽车 bus: 公交车 love: 爱 hate: 恨 hello: 你好 goodbye: 再见 summer: 夏天 winter: 冬天
Dict.hpp
#pragma once #include <iostream> #include <string> #include <fstream> #include <unordered_map> #include <unistd.h> #include "Log.hpp"using namespace log_ns;const static std::string sep = ": ";class Dict { private:void LoadDict(const std::string &path){std::ifstream in(path);if (!in.is_open()){LOG(FATAL, "open %s failed!\n", path.c_str());exit(1);}std::string line;while (std::getline(in, line)){LOG(DEBUG, "load info:%s, success\n", line.c_str());if (line.empty())continue; // 是空行,继续// 对字符串进行处理auto pos = line.find(sep); // 查取分隔符// 这一行都查完了,都没有找到分隔符,说明不满足条件,直接跳过if (pos == std::string::npos)continue;// apple: 苹果// 因此在0号下标到冒号位置都是单词所在的区间(前闭后开std::string key = line.substr(0, pos);if (key.empty())continue;std::string value = line.substr(pos + sep.size());if (value.empty()) continue;_dict.insert(std::make_pair(key, value));}LOG(INFO, "load %s done\n", path.c_str());in.close();}public:Dict(const std::string &dict_path) : _dict_path(dict_path){LoadDict(_dict_path);}// 翻译std::string Translate(std::string word){if (word.empty())return "None";auto iter = _dict.find(word);if (iter == _dict.end())return "None";elsereturn iter->second; //也就是 value}~Dict(){}private:// 字典std::unordered_map<std::string, std::string> _dict;// 字典路径std::string _dict_path; };
UdpServerMain.cc
#include "UdpServer.hpp" #include<memory> #include"Dict.hpp" using namespace log_ns;// ./udp_server local-port int main(int argc, char *argv[]) {if (argc != 2){std::cerr << "Usage:" << argv[0] << "local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);EnableScreen();Dict dict("./dict.txt");func_t translate = std::bind(&Dict::Translate, &dict, std::placeholders::_1);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(translate, port);//C++14//初始化服务器usvr->InitServer();usvr->Start();return 0; }
效果:
V3 版本 - 简单聊天室
将线程池引入【Linux】多线程日志系统的封装、单例模式优化线程池代码编写-CSDN博客
这篇博客中有线程池完整代码
具体思路:
让udpserver读取数据,用户的iP地址加端口号来表示客户端的唯一性,我们需要维护一个在线用户列表,udpserver将读取到的数据交给转发与发送模块,转发与发送模块再把数据转到路由和转发模块再根据在线用户列表,转发给所有人。
以线程池的方式转发
首先我们创建函数类型,返回值为void,参数为套接字描述符,服务端收到的消息,以及套接字信息:并且创建func_t _func对象,构造函数初始化_func
using func_t = std::function<void(int, const std::string message, InetAddr &who)>;
Route.hpp
#pragma once#include <iostream> #include <string> #include <vector> #include <sys/socket.h> #include <sys/types.h> #include <functional>#include "InetAddr.hpp" #include"ThreadPool.hpp" // class user//这里的任务在线程池里。只需要t()来运行任务,因此是不传任何参数的,返回值也是void using task_t = std::function<void()>;class Route {public:Route(){}void CheckOnlineUser(InetAddr &who){for (auto &user : _online_user){if (user == who){LOG(DEBUG, "%s is exists\n", who.AddrStr().c_str());return;}}LOG(DEBUG, "%s is not exists, add it\n", who.AddrStr().c_str());_online_user.push_back(who);}void OffLine(InetAddr &who){auto iter = _online_user.begin();for (; iter != _online_user.end(); iter++){if (*iter == who){LOG(DEBUG, "%s is offline\n", who.AddrStr().c_str());_online_user.erase(iter);break;}}}// 做转发void ForwardHelper(int sockfd, const std::string message){for (auto &user : _online_user){struct sockaddr_in peer = user.Addr();LOG(DEBUG, "forward message to %s, message is %s", user.AddrStr(), message.c_str());::sendto(sockfd, message.c_str(), sizeof(message), 0, (struct sockaddr *)&peer, sizeof(peer));}}void Forward(int sockfd, const std::string &message, InetAddr &who){// 1.该用户是否在 在线用户列表中呢,如果在,什么都不做,不在就自动添加到_online_user列表里CheckOnlineUser(who);// 1.1 message == "QUIT" "Q"if (message == "QUIT" || message == "Q"){OffLine(who);}// 2.who 一定在_online_user列表里// ForwardHelper(sockfd, message);//获取线程池对象:将任务添加到任务队列task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message);ThreadPool<task_t>::GetInstance()->Equeue(t);}~Route(){}private:std::vector<InetAddr> _online_user; };
UdpServerMain.cc
#include "UdpServer.hpp" #include "Route.hpp" #include <memory>using namespace log_ns;// ./udp_server local-port int main(int argc, char *argv[]) {if (argc != 2){std::cerr << "Usage:" << argv[0] << "local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);EnableScreen();Route messageRoute;service_t message_route = std::bind(&Route::Forward, &messageRoute\, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(message_route, port); // C++14// 初始化服务器usvr->InitServer();usvr->Start();return 0; }
修改InetAddr.hpp:
#pragma once #include <iostream> #include <string> #include <cstring> // 网络套接字编写的时候需要用到的头文件 #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h>class InetAddr { private:void ToHost(const struct sockaddr_in &addr){_port = ntohs(addr.sin_port); // 这是从网络过来的因此要做处理_ip = inet_ntoa(addr.sin_addr);}public:InetAddr(const struct sockaddr_in &addr) : _addr(addr){ToHost(addr);}bool operator==(const InetAddr &addr){return (this->_ip == addr._ip && this->_port == addr._port);}std::string Ip(){return _ip;}std::string AddrStr(){return _ip + ":" + std::to_string(_port);}uint16_t Port(){return _port;}struct sockaddr_in Addr(){return _addr;}~InetAddr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr; };
运行结果:明显发现,出现消息打出错误,这是因为我们没有考虑到线程安全的问题
因为我们现在做的是本地测试,所以报文会被挤压在OS内部,同一个套接字,是支持同时读写的。因此我们需要调整客户端。客户端一定要有两个线程,一个来做消息的读取,一个来做消息的发送,因此需要有一个发送消息的线程(键盘获取数据 + 显示终端,将数据发给服务器),一个做接收消息的线程(网络获取消息,显示到另一个终端)。
重新封装客户端代码:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Thread.hpp"
using namespace ThreadModle;// 封装套接字的创建
int InitClient()
{// 创建客户端套接字int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "create socket error" << std::endl;exit(1);}return sockfd;
}void RecvMessage(int sockfd, const std::string &name)
{//客户端在不发消息的时候,也会一直在收消息while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);char buffer[1024];// udp客户端是不面向连接的,一般有多个服务端向客户端发消息,因此客户端需要区分发消息的是谁int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0;std::cerr << buffer << std::endl;}else{std::cout << "recvfrom error" << std::endl;break;}}
}// 需要知道目的方的IP和端口号
void SendMessage(int sockfd, std::string serverip, uint16_t serverport, const std::string &name)
{// 获取服务器对应的套接字信息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());std::string cli_profix = name + "# ";while (true){std::string line;std::cout << cli_profix;std::getline(std::cin, line);// 给服务器发送消息int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));if (n <= 0){break;}}
}int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage:" << argv[0] << "server-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1.初始化客户端int sockfd = InitClient();// 2.启动线程Thread recver("recver-thread", std::bind(&RecvMessage, sockfd, std::placeholders::_1));Thread sender("sender-thread", std::bind(&SendMessage, sockfd, serverip, serverport, std::placeholders::_1));recver.Start();sender.Start();recver.Join();sender.Join();::close(sockfd);return 0;
}
也是为了确认自己发出去的消息确实被服务端收到了,现在是为了让每一个在发消息的客户端,都能看到对方所发送的消息
为了做实验,将客户端收到的消息,打印到标准错误里面(2)
当存在一个客户端的时候:也可以直接重定向到自己创建的管道里
存在两个客户端的时候:
管道的做法:
mkfifo pipe cat < pipe
优化,给收到的消息添加是谁:
优化位置:
运行结果:
_online_user实际上就是一个临界资源。怕的是有人在进行读取数据的时候,有人在检查User是否已经在user列表,因此要加锁
最终路由Route.hpp代码:
#pragma once#include <iostream> #include <string> #include <vector> #include <sys/socket.h> #include <sys/types.h> #include <functional>#include "InetAddr.hpp" #include"ThreadPool.hpp" #include "LockGuard.hpp" // class user//这里的任务在线程池里。只需要t()来运行任务,因此是不传任何参数的,返回值也是void using task_t = std::function<void()>;class Route {public:Route(){pthread_mutex_init(&_mutex, nullptr);}void CheckOnlineUser(InetAddr &who){LockGuard lockguard(&_mutex);for (auto &user : _online_user){if (user == who){LOG(DEBUG, "%s is exists\n", who.AddrStr().c_str());return;}}LOG(DEBUG, "%s is not exists, add it\n", who.AddrStr().c_str());_online_user.push_back(who);}void OffLine(InetAddr &who){LockGuard lockguard(&_mutex);auto iter = _online_user.begin();for (; iter != _online_user.end(); iter++){if (*iter == who){LOG(DEBUG, "%s is offline\n", who.AddrStr().c_str());_online_user.erase(iter);break;}}}// 做转发void ForwardHelper(int sockfd, const std::string message, InetAddr who){LockGuard lockguard(&_mutex);//携带客户端信息std::string send_message = "[" + who.AddrStr() + "]#" + message;for (auto &user : _online_user){struct sockaddr_in peer = user.Addr();LOG(DEBUG, "forward message to %s, message is %s", user.AddrStr(), send_message.c_str());::sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));}}void Forward(int sockfd, const std::string &message, InetAddr &who){// 1.该用户是否在 在线用户列表中呢,如果在,什么都不做,不在就自动添加到_online_user列表里CheckOnlineUser(who);// 1.1 message == "QUIT" "Q"if (message == "QUIT" || message == "Q"){OffLine(who);}// 2.who 一定在_online_user列表里// ForwardHelper(sockfd, message);//获取线程池对象:将任务添加到任务队列task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message, who);ThreadPool<task_t>::GetInstance()->Equeue(t);}~Route(){pthread_mutex_destroy(&_mutex);}private:std::vector<InetAddr> _online_user; pthread_mutex_t _mutex; };
运行结果:
inet_ntoa
inet_ntoa 这个函数返回了一个 char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果. 那么是否需要调用者手动释放呢?
因为 inet_ntoa 把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果
运行结果:
在多线程环境下, 推荐使用 inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题
因此:我们修改InetAddr代码:
void ToHost(const struct sockaddr_in &addr){_port = ntohs(addr.sin_port); // 这是从网络过来的因此要做处理// _ip = inet_ntoa(addr.sin_addr);char ip_buf[32];::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));_ip = ip_buf;//网络序列转成字符串风格的}
使用方式: