当前位置: 首页 > ai >正文

【计算机网络】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 结构的指针,包含本地地址信息

    • addrlenaddr 的长度。

  • 返回值:成功返回 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;//网络序列转成字符串风格的}

使用方式:

http://www.xdnf.cn/news/2359.html

相关文章:

  • UML 活动图详解之小轿车启动活动图分析
  • 【dockerredis】用docker容器运行单机redis
  • ASP.NET图片盗链防护指南
  • Java接口默认方法冲突
  • 2025.4.27_C_Struct,Enum,Union
  • 单片机学习笔记9.数码管
  • Redis使用总结
  • 相机DreamCamera2录像模式适配尺寸
  • 使用c++实现一个简易的量子计算,并向外提供服务
  • 一文说清Token这个大模型中的数字乐高积木的作用
  • MIT6.S081 - Lab10 mmap(文件内存映射)
  • 内耗型选手如何能做到不内耗?
  • MySQL最新安装、连接、卸载教程(Windows下)
  • Linux进程学习【环境变量】进程优先级
  • T8332FN凯钰LED驱动芯片多拓扑车规级AEC-Q100
  • 秒杀压测计划 + Kafka 分区设计参考
  • IP地址与子网计算工具
  • 0302洛必达法则-微分中值定理与导数的应用.md
  • 云原生课程-Docker
  • openstack创建虚拟机
  • 什么是模块化区块链?Polkadot 架构解析
  • 在Linux中,使用标准IO库,进行格式化IO操作
  • 深度解析Zemax优化函数:让光学设计从“能用”到“极致”的核心密码
  • 驱动开发硬核特训 · Day 22(下篇): # 深入理解 Power-domain 框架:概念、功能与完整代码剖析
  • I-CON: A Unifying Framework for Representation Learning
  • qt 3d航迹图
  • Scala集合操作与WordCount案例实战总结
  • Linux高效IO
  • SQL面试之--明明建了索引为什么失效了?
  • docker部署ruoyi系统