【Linux篇章】Socket 套接字,竟让 UDP 网络通信如此丝滑,成为一招致胜的秘籍!
本篇文章将带大家了解网络通信是如何进行的(如包括网络字节序,端口号,协议等) ;再对socket套接字进行介绍;以及一些udp-socket相关网络通信接口的介绍及使用;最后进行对基于udp的网络通信(服务端与客户端之间)进行测试和外加模拟实现的基于udp通信的简单的英译汉词典与多人聊天室;欢迎阅读!!!
欢迎拜访:羑悻的小杀马特.-CSDN博客
本篇主题:速通基于UDP的简单网络通信
制作日期:2025.07.08
隶属专栏:Linux之旅
目录
一·网络通信相关概念:
1.1认识ip及端口号:
认识ip:
认识端口号:
端口号划分:
端口号与进程ID的区别:
理解源端口号与目标端口号:
1.2浅解Socket概念:
浅浅认识TCP/UDP协议:
TCP协议:
UDP协议:
认识网络字节序:
认识Socket底层结构:
认识sockaddr_in 结构:
二·网络通信之Socket-UDP:
2.1Socket-UDP相关网络通信接口函数认识:
2.1.1创建套接字之socket接口:
2.1.2绑定套接字之bind接口:
2.1.3 接收信息之recvfrom接口:
2.1.4发送信息之sendto接口:
2.2UDP通信特点及注意事项:
UDP通信特性:
UDP通信时注意事项:
①客户端是不需要手动绑定:
②一般服务端是不能直接绑定特定ip的:
2.3实现基于UDP的server-client简单通信:
测试效果:
代码实现:
主要代码文件部分:
udpclient.cc:
udpserver.cc:
udpserver.hpp:
间接包含文件:
log.hpp:
Makefile:
mutex.hpp:
2.4基于UDP实现的server-client简单通信改造的英译汉翻译功能:
测试效果:
代码实现:
主要代码部分:
dict.hpp:
dict.txt:
addr.hpp:
udpclient.cc:
udpserver.cc:
udpserver.hpp:
间接包含部分:
2.5基于UDP实现的server-clients通信改造多人聊天室(服务端多线程版本):
效果展示:
代码实现:
主要代码部分:
udpclient.cc:
udpserver.cc:
udpserver.hpp:
addr.hpp:
route.hpp:
间接包含部分:
cond.hpp:
thread.hpp:
threadpoll.hpp:
优化关于网络序列和本机序列之间的转化:
之前对port的网络本机序列转化:
之前对ip的网络本机序列转化:
三·常用的网络指令:
3.1ifconfig:
3.2netstat:
3.3ping:
3.4pidof:
四·本篇小结:
一·网络通信相关概念:
1.1认识ip及端口号:
首先,何为网络通信:
数据传输到主机不是目的,而是手段。到达主机内部,在交给主机内的进程,才是目的!
因此上网要么从某处读取数据,要么向某处发送数据;本质就是数据的交互!因此我们可以理解成:
故通信/数据通过网络传输其实就是两个指定进程之间通过网络传输!
认识ip:
IP地址是在IP协议中,用来标识网络中不同主机的地址;对于IPv4来说,IP地址是一个4字节,32位的整数;我们通常也使用\"点分十进制\"的字符串表示IP地址,例如192.168.0.1;用点分割的每一个数字表示一个字节,范围是0-255;这里我们就认为是区域内标定唯一主机的!
认识端口号:
端口号是一个 2 字节 16 位的整数; 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理!因此可以总结成一个端口号只能对应一个进程;但是一个进程可以有多个端口号! 端口号目的就是从主机中确定一个具体的进程!
端口号划分:
0-1023: 知名端口号, HTTP,FTP,SSH 等这些广为使用的应用层协议,他们的端口号都是固定的。
1024-65535:操作系统动态分配的端口号.客户端程序的端口号,就是由操作系统从这个范围分配的。也就是说我们在后面udptcp通信等使用的端口号都是1024-65535这个范围内的!
端口号与进程ID的区别:
端口号是进程唯一性的标识也就是说通过指定端口号只能找到一个进程另外,一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定!
形象理解下:
可以把进程想象成不同的商店,端口号就像是商店的门牌号。
一个进程可以绑定多个端口号,就好比一家商店可以有多个门,比如大型商场,它可能有正门、侧门、后门等多个门(多个端口号),顾客(数据)可以从不同的门进入商场(进程),每个门都可以用来接待不同类型的顾客或者提供不同的服务。
而一个端口号不能被多个进程绑定,是因为如果一个门牌号对应了多家商店,那么顾客就会不知道该进哪家店,数据也会混乱,不知道该把信息送到哪个进程。所以,一个门牌号(端口号)只能对应一家商店(进程),这样才能保证数据准确无误地到达对应的进程。
进程 ID 属于系统概念,技术上也具有唯一性,确实可以用来标识唯一的一个进程,但是这样做,会让系统进程管理和网络强耦合,实际设计的时候,并没有选择这样做。
不是所有的进程,都要进行网络通信;因此端口号不是每个进程都有的; 这里pid是一个系统概念;端口号是网络概念;当网络一变那么进程对应的端口号就会变化;但是pid是不变的;实现了解耦!
理解源端口号与目标端口号:
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号.就是在描述\"数据是谁发的,要发给谁\"。
得出结论:IP+Port=全网内唯一的一个进程;
因此,就可以理解网络通信本质:全网内唯二的两个进程在进行进程间通信,其中ip确定好网络内一主机;而端口号就确定的是该主机的某个进程;要完成网络通信我们就需要:源ip,源port,目的ip,目的端口号,也就是源socket和目标socket;我们在下面会讲到!
1.2浅解Socket概念:
在上面我们也讲到了,IP 地址用来标识互联网中唯一的一台主机,port 用来标识该主机上唯一的.一个网络进程!
通信的时候,本质是两个互联网进程代表人来进行通信,{srclp,srcPort,dstlp,dstPor}这样的4元组就能标识互联网中唯二的两个进程;网络通信就是进程间通信!
我们把币p+port 叫做套接字 socket ;这就是它的由来!
socket和tcp/ip有关也就是和网络层传输层有关-->属于内核-->受OS控制-->需要使用系统接口:
因此当我们使用套接字的时候就需要使用系统提供的接口;后面用的时候我们会讲解!
浅浅认识TCP/UDP协议:
TCP协议:
TCP(Transmission Control Protocol 传输控制协议):
特点:
做更多工作,复杂,占有资源多
传输层协议
有连接
可靠传输
面向字节流(类似当初的文件流;耦合性不是特别强;这里我们将TCP通信的时候会明显察觉)
UDP协议:
UDP(User Datagram Protoco 用户数据报协议):
特点:
相对做的少;简单等
传输层协议
不可靠传输
面向数据报(报比如发送10个信息就收到10个不多也不少;马上就可以看到了)
这里我们仍旧是需要保存他们俩;各有各的特点;谁更合适就选择谁!!!
认识网络字节序:
先认识下大小端:
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
因此就有了以下规定:
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址;其中TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节。
大小端机器 都会遵循这个原则;如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可(只不过应用了转化技术)。
但是每次这样发送和接收的时候我们都进行转化相对比较麻烦;因此系统就自己封装了一些函数;自动帮助我们是被本端机器大小端完成网络和本地的转化工作!
助记:
h 表示 host,n 表示 network,表示 32 位长整数,s 表示 16 位短整数。
大小端转化问题:
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
说白了就是网络通信要用网络字节序;其他地方就是主行,可以调用以下库函数做网络字节序和主机字节序的转换。机字节序: 主机字节序-->网络字节序-->主机字节序!
不仅有 1·本地序列和网络序列转化功能 2·还兼容大小端转化问题->完美!
后面我们应用的时候再做演示说明!
认识Socket底层结构:
首先,先来看一下常见API:
先认识下后面我们在UDP;TCP通信的时候都会用到!
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址.
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6.这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
socket API可以都用struct sockaddr*类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4,IPv6,以及UNIX DomainSocket各种类型的sockaddr结构体指针做为参数;
总而言之;我们接下来就只拿IPV4地址类型来谈;利用它的类型定义及结构体!
下面我们看张图理解下对应的IPV4下的本地与网络通信:
这里socket为什么不强转void+传进去:当时语言还不支持;而这样写更加能体现出继承多态那套逻辑。
但是上面我们说了,无论是网络通信还是本地通信最后结构体都会被转出sockaddr形式;那么它该如何区分是网络通信还是本地通信呢?
判断首地址为哪个类型就强转成那个类型指针去访问!!!
下面我们利用文字叙述下上面操作的过程:
首先把un或者in的地址传给这个所谓的\'父类\':然后它的address的值就被覆盖成对应传进来的结构体的首地址了﹔但是类型还是sockaddra因此当我们拿对应指针访问sockaddr的成员的时候(就是相当于加上之前拿成员对应的移动多少地址的匹配对应关系)-->故访问到的就是前16位;因此拿着个里面的数据去判断﹔如果是AFINET就转成对应的指针﹔此时首地址还是没有变;只是它的类型变成了对应的sockaddr_in类型就可以访问了﹔改变指针类型==改变了这种访问成员的偏移量的匹配机制
因为网络通信和本地通信大差不差;也就是理解了网络通信;本地通信就差不多了;那么下面我们就学习一下网络通信:
认识sockaddr_in 结构:
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)]; };
这里虽然看起来很多参数;大都是typedef出来的;因此不必担心!
下面我们看张图;来更清晰认识它:
这样我们就和之前的那张继承的结构体图片为什么那样写就完美结合底层结构认识了!!!
这里我们就先简单认识下IPV4地址下的网络通信的底层结构即可;后面我们在使用这些接口的时候会有更清晰认识的!!!
二·网络通信之Socket-UDP:
2.1Socket-UDP相关网络通信接口函数认识:
下面我们将为之后基于UDP实现的网络通信先进行相关函数接口介绍:
首先就是网络通信接口必备四大头文件:
#include #include #include #include
2.1.1创建套接字之socket接口:
#include /* See NOTES */ #include int socket(int domain, int type, int protocol);
成功返回套接字的文件描述符;失败就小于0!
对于UDP我们这样用:
这里其实返回的这个socketfd就是个文件描述符:因此可以理解成我们这样就创建了一个“网络通信的文件”。
2.1.2绑定套接字之bind接口:
#include /* See NOTES */ #include int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这里的socklen_t就是这个结构体的长度字节!
成功返回0;失败就小于0!
需要注意的是我们网络通信的sockaddr_in*需要强转成sockaddr*类型 !
在我们填写sockaddr_in的时候需要注意的是:
注意本地序列与网络序列之间的转化:
此时就用到我们上面讲述的htons了:把这个16位整数按照1·大端的方式2·变成网络序列
#include uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
以及ip地址转化inet_addr:系统提供的可以把string字符串类型转化成1·大端;2、·并且转化成32位数字的网络序列
in_addr_t inet_addr(const char *cp);
这里一般绑定是对于服务端而言;而客户端无需绑定(系统自己根据分配进行绑定);然而一般服务端是允许它所可以识别的多个对应ip发起请求的;因此不建议绑死固定ip;后面会讲述!
对于ip地址由本地的字符串样式转化成网络四字节序列模拟操作:
上面过程只需要了解即可!!!
这里我们提供了inet_addr及inet_pton/addr_ntop等接口大大减轻了我们的负担!!!
2.1.3 接收信息之recvfrom接口:
#include #include ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
一般使用的阻塞式用法;成功就返回读到的个数;失败就小于0;下面我们结合使用来理解下各个参数:
我们拿到发送方的ip+port是网络序列;可以转化成本地进行查看:
2.1.4发送信息之sendto接口:
#include #include ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
成功返回0;失败就小于0;和上面的那个recvfrom差不多;但是上面传递的sockaddr_in是输出型参数;这里是输入型参数 !(一些细节地方注意一下;比如取地址等等(一般只有需要修改的时候才会取地址;如len但是sockaddr无论咋样都是传递地址))
如;
上面这些接口对应我们的udp通信就够用了;其他的就是相关其他转化接口了;我们会在后面书写通信代码的时候提到(如htons系列,inet_addr等)!
2.2UDP通信特点及注意事项:
UDP通信特性:
udp sockfd,既可以读,又可以写。UDP通信,其实是全双工的;只要有src+des的ip+port就可以发送;这里只有接收缓冲区;无发送缓冲区;对于udp(和tcp不同,后面讲的tcp会讲解如何处理),只要发了多少就会一次性读完不会出现“粘报”等情况!
UDP通信时注意事项:
这里我们拿客户端然后用des的ip和port(服务端绑定的)就可以给服务端发信息;客户端本身就绑定了对应服务器的des ip port等等!(服务端与客户端是一家公司写的!)
公网IP其实没有配置到你的IP上。公网IP无法被直接bind;我们bind的要么是本地环回要么就是子网ip!
①客户端是不需要手动绑定:
os自己认识用户端机器的ip+随机会生成端口号port自动绑定与解除;故无需bind/程序终止立刻解除故查不到状态!
主机换了个区域的网络,通常会获得一个对应新网络的子网IP;因此如果我们在某个区域进行网络通信;就会把这个区域分配的子网ip+port绑定然后通信;如果我们还要在另一个不同的网络区域通信那么就又要解绑再次更换ip+port了有点麻烦!!!---->因此oS就支持了自动分配﹐((因为我们如果给服务器进行网络通信-->因此客户端就会首先根据对应主机所在网络区域分配的ip然后进行自己的绑定-->再拿到对应客户端内置的服务端的iptport进行发送即可)
下面我们举一个形象例子帮助理解下:
你进入图书馆时,不需要自己去指定要去哪个书架(手动绑定IP地址)以及具体要坐在书架的什么位置看书(手动绑定端口)。因为图书馆有管理员,他们会根据图书馆的空间使用情况和你的需求,给你安排一个合适的书架和座位,这就像网络中的 DHCP 服务器和操作系统,它们会自动为客户端分配可用的IP地址和端口号。
②一般服务端是不能直接绑定特定ip的:
如果绑定了:给服务端发信息就只能通过这个绑定的ip+port进行了!
但是一般这么用,服务端所在的主机会为自己的客户端内置好自己的主机的一些ip;然后当客户端去给它发信息就可以拿着不同的ip(但是终归是服务端所在同一主机的ip)去给服务端发送信息;服务端识别到属于对应ip族;然后port也相同故可以接收....
2.3实现基于UDP的server-client简单通信:
下面,我们就基于上面所介绍的接口函数以及注意事项来完成简单通信;在我们书写代码前先看一下,这个模型的形象图:
下面我们就拿字节旗下的app如抖音等等基于这个模型解释下:
比如用户在抖音 ...字节产品的app用户端进行访问自己的数据(是首先要和字节的服务器进行网络通信的;然后服务器发送过来);此时的网络通信就可以理解成上面所讲的;首先因为这些app是字节的肯定内置了字节对服务器的ip(可能不同;但是都是可以找到对应的服务器【由于服务器绑定的是INADDR ANY】);然后0S根据自己所在网络ip进行客户端绑定;然后进行通信;最后服务器再给返回来;然后可能有多个用户端进行访问也是可以同时访问成功服务器拿到数据的!!!
多个app同时进行也是一样的(套接字最终绑定的是进程)
测试效果:
代码实现:
详细说明见代码超详细注释:
主要代码文件部分:
udpclient.cc:
#include #include #include #include #include #include #include #include \"log.hpp\"using namespace std;int main(int argc, char *argv[]){ if (argc != 3) { cerr << \" please use: \" << argv[0] << \" server_ip server_port\" << std::endl; return 1; } // 创建套接字: int sd = socket(AF_INET, SOCK_DGRAM, 0); // 网络通信/用户数据报 if (sd < 0) use_log(loglevel::DEBUG) << \"socket failure!\"; use_log(loglevel::DEBUG) << \"socket success!\"; // os自己认识用户端机器的ip+随机会生成端口号port自动绑定与解除;故无需bind/程序终止立刻解除故查不到状态 string ip = argv[1]; // string内置机制不会直接拷贝指针 uint16_t port = stoi(argv[2]); // 先构造string //初始化des套接字: 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());//死循环式等待给服务端发送信息: while (1) { string mess; getline(cin, mess); socklen_t len = sizeof(local); //发送: int so = sendto(sd, mess.c_str(), mess.size(), 0, (sockaddr *)&local, len); // 输入型参数 if (so < 0) use_log(loglevel::DEBUG) << \"sendto failure!\"; //接收: char buff[1024] = {0}; ssize_t rm= recvfrom(sd, buff, sizeof(buff) - 1, 0, (sockaddr *)&local, &len); if (rm< 0) use_log(loglevel::DEBUG) << \"recvfrom failure!\"; cout<<\"$server say:\"<< buff<<endl;; }}
udpserver.cc:
#include\"udpserver.hpp\"string echo_repeat(string mess){ string ret; ret+=\"return \"; ret+=mess; return ret;}int main(int argc ,char* argv[]){ // if(argc != 3) // { // std::cerr << \"please use: \" << argv[0] << \" ip\"<<\" port\" << std::endl; // return 1; // } if(argc != 2) { std::cerr << \"please use: \" << argv[0] <<\" port\" << std::endl; return 1; } consolestrategy; //string ip=argv[1];//string内置机制不会直接拷贝指针 // uint16_t port=stoi(argv[2]);//先构造string uint16_t port=stoi(argv[1]); // unique_ptr ur=make_unique(ip,port); //unique_ptr ur=make_unique(port); //回调: unique_ptr ur=make_unique(port,echo_repeat); ur->init(); ur->start(); }
udpserver.hpp:
#include #include #include #include #include #include #include#include#include \"log.hpp\"using namespace std;const int delfd = -1;using func_t=function;// consolestrategy; 不允许全局直接使用操作;只允许定义声明等等class udpserver{public: // udpserver(string ip, uint16_t port) : _ip(ip), _port(port), // _isrunning(0), _socketfd(delfd) {} //这里服务器不能绑死否则只能接受指定的主机发来的信息了 //udpserver( uint16_t port) : _port(port), _isrunning(0), _socketfd(delfd) {} udpserver( uint16_t port,func_t func) : _port(port), _isrunning(0), _socketfd(delfd),_func(func) {} void init() { // 1`创建套接字: _socketfd = socket(AF_INET, SOCK_DGRAM, 0); // 网络通信/用户数据报 if (_socketfd < 0) use_log(loglevel::DEBUG) << \"socket failure!\"; use_log(loglevel::DEBUG) << \"socket success!\"; // 2`socket绑定信息: sockaddr_in local; // char sin_zero[8]; // 填充字节,使 sockaddr_in 和 sockaddr 长度相同 bzero(&local, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); // 把对应的16位转化成网络序列 // 内置的把string转化成大端类型的网络序列;也可手动操作(来回转化比较麻烦系统直接提供) // ntohs()网络序列转回本地 //. local.sin_addr.s_addr = inet_addr(_ip.c_str()); local.sin_addr.s_addr =INADDR_ANY ; // 或者直接输入0;可以理解成匹配同一主机的不同ip的任意des-port相同的不同进程发来的信息 int n = bind(_socketfd, (sockaddr *)&local, sizeof(local)); if (n < 0) use_log(loglevel::DEBUG) << \"bind failure!\"; use_log(loglevel::DEBUG) <死循环: _isrunning = 1; while (_isrunning) { char buff[1024] = {0}; sockaddr_in per; socklen_t len = sizeof(per); // 套接字长度就是字节数 ssize_t rm = recvfrom(_socketfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&per, &len); // 输出型参数故取地址 if (rm< 0) use_log(loglevel::DEBUG) << \"recvfrom failure!\"; buff[rm]=0; string per_addr= inet_ntoa(per.sin_addr); uint16_t per_port=ntohs(per.sin_port); cout<< \"$client :[addr: \"<<per_addr<<\" port: \"<<per_port<<\" ] say: \"<<buff<<endl; //string res=\"server say:\"; // res+=buff; //int so=sendto(_socketfd,res.c_str(),res.size(),0,(sockaddr *)&per,len);//这里接收两个全都对套接字用的是指针:继承多态效果 string ans= _func(buff); int so=sendto(_socketfd,ans.c_str(),ans.size(),0,(sockaddr *)&per,len);//这里接收两个全都对套接字用的是指针:继承多态效果 //输入型参数 if (so < 0) use_log(loglevel::DEBUG) << \"sendto failure!\"; } } ~udpserver() { }private: //string _ip; uint16_t _port; int _socketfd; bool _isrunning; func_t _func;};
间接包含文件:
log.hpp:
#ifndef __LOG__#define __LOG__#include #include #include #include #include #include #include #include #include#include#include \"mutex.hpp\"using namespace std;#define gsep \"\\r\\n\"// 基类:class Logstrategy{public: Logstrategy() {} virtual void synclog(const string &message) = 0; ~Logstrategy() {}};// 控制台打印日志:class consolelogstrategy : public Logstrategy{public: consolelogstrategy() {} void synclog(const string &message) override { // 加锁完成多线程互斥: { mutexguard md(_mutex); cout << message << gsep; } } ~consolelogstrategy() {}private: mutex _mutex;};// 自定义文件打印日志:const string P = \"./log\";const string F = \"my.log\";class fileLogstrategy : public Logstrategy{public: fileLogstrategy(const string path = P, const string file = F) : _path(path), _file(file) { // 如果指定路径(目录)不存在进行创建;否则构造直接返回: { mutexguard md(_mutex); if (filesystem::exists(_path)) return; try { filesystem::create_directories(_path); } catch (filesystem::filesystem_error &e) { cout << e.what() << gsep; } } } void synclog(const string &message) override { // 得到指定文件名: { mutexguard md(_mutex); string name = _path + (_path.back() == \'/\' ? \"\" : \"/\") + _file; // 打开文件进行<<写入: ofstream out(name, ios::app); // 对某文件进行操作的类对象 if (!out.is_open()) return; // 成功打开 out << message << gsep; out.close(); } } ~fileLogstrategy() {}private: string _path; string _file; mutex _mutex;};// 用户调用日志+指定打印:// 日志等级:enum class loglevel{ DEBUG, INFO, WARNING, ERROR, FATAL};// 完成枚举值对应由数字到原值转化:string trans(loglevel &lev){ switch (lev) { case loglevel::DEBUG: return \"DEBUG\"; case loglevel::INFO: return \"INFO\"; case loglevel::WARNING: return \"WARNING\"; case loglevel::ERROR: return \"ERROR\"; case loglevel::FATAL: return \"FATAL\"; default: return \"ERROR\"; } return\"\";}// 从时间戳提取出当前时间:string gettime(){ time_t curtime=time(nullptr); struct tm t; localtime_r(&curtime,&t); char buff[1024]; sprintf(buff,\"%4d-%02d-%02d %02d:%02d:%02d\", t.tm_year+1900,//注意struct tm成员性质 t.tm_mon+1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec ); return buff; }class Log{public: // Log刷新策略: void console() { _fflush_strategy = make_unique(); } void file() { _fflush_strategy = make_unique(); } Log() { // 默认是控制台刷新: console(); } // 我们想让一个类重载了<<支持连续的<内部类天然就是外部类的友元类;可以访问外部类所有成员变量及函数 class Logmess { public: Logmess(loglevel &lev, string filename, int line, Log &log) : _lev(lev), _time(gettime()), _pid(getpid()), _filename(filename), _log(log), _linenum(line) { stringstream ss; ss << \"[\" << _time << \"] \" << \"[\" << trans(_lev) << \"] \" << \"[\" << _pid << \"] \" << \"[\" << _filename << \"] \" << \"[\" << _linenum << \"] \" << \" \"; _mergeinfo=ss.str(); } template Logmess& operator <<(const T& data){ stringstream ss; ss<synclog(_mergeinfo); } private: loglevel _lev; string _time; pid_t _pid; string _filename; int _linenum; string _mergeinfo; Log &_log; }; Logmess operator()(loglevel l,string f,int le) { //返回的是匿名对象(临时对象)-->也就是作用完当前行 //(执行完logmess的<< <<后自动调用logmess的析构也就是直接策略打印) return Logmess(l,f,le,*this); } ~Log() {}private: unique_ptr _fflush_strategy;}; Log l; #define use_log(x) l(x,__FILE__,__LINE__)//自动判断是哪行哪个文件 #define filestrategy l.file() #define consolestrategy l.console()#endif
Makefile:
.PHONY:allall:udpclient udpserverudpclient:udpclient.ccg++ -o $@ $^ -std=c++17 udpserver:udpserver.ccg++ -o $@ $^ -std=c++17 .PHONY:cleanclean:rm -f udpclient udpserver
mutex.hpp:
#pragma once#include//封装锁:class mutex{ public: mutex(){ int n= pthread_mutex_init(&_mutex,nullptr); (void)n; } void Lock(){ pthread_mutex_lock(&_mutex);} void Unlock(){ pthread_mutex_unlock(&_mutex);} pthread_mutex_t*getmutex(){return &_mutex;} ~mutex(){ int n= pthread_mutex_destroy(&_mutex); (void)n; }private: pthread_mutex_t _mutex;};//自动上锁与解锁class mutexguard{ public: //初始化为上锁; mutexguard(mutex &mg):_mg(mg){ _mg.Lock() ; }//引用 //析构为解锁: ~mutexguard(){_mg.Unlock() ; } private: mutex &_mg;//注意引用:确保不同线程上锁与解锁的时候拿到同一把锁;不能是直接赋值};
2.4基于UDP实现的server-client简单通信改造的英译汉翻译功能:
这里实现翻译的功能;只不过是给服务端接收到信息进行多了一个回调的函数来完成的;所以改动不大;相当于加个回调方法即可。
实现思路:
首先服务端进行词典的加载也就是调用査询的函数内部把对应的英语和汉语的对应关系放入map里;然后用户端给dict. hpp:服务端发送对应的英文串;然后服务端拿着对应的英文串通过回调去査询函数寻找second;找到了就sendto回去对应second:否则sendto none!!!
下面我们增加了个翻译功能的类使得能从指定文件中加载对应映射关系;以及处理功能等!
测试效果:
先loading词典到本地:
下面等待用户端输入;进行查询发出:
代码实现:
主要代码部分:
dict.hpp:
#pragma once#include #include #include #include #include \"log.hpp\"#include \"addr.hpp\"using namespace std;const string sep = \": \";string pathname = \"./dict.txt\";class dict{public: dict(const string p = pathname) : _pathname(p) {} bool loaddict() { ifstream in(_pathname); if (!in.is_open()) { use_log(loglevel::DEBUG) << \"打开字典: \" << _pathname << \" 错误\"; return false; } string line; while (getline(in, line)) { int pos = line.find(sep); if(pos==string::npos) { use_log(loglevel::DEBUG) << \"当前加载错误,继续向下加载\"; continue; } string word = line.substr(0, pos); string chinese = line.substr(pos + 2,string::npos); if (word.size() == 0 || chinese.size() == 0) { use_log(loglevel::DEBUG) << \"加载错误\"; continue; } _dict.insert({word, chinese}); use_log(loglevel::DEBUG) << \"成功加载:\"<<line;; } in.close(); return true; } string translate(const string src, inetaddr client)//const类型对象只能调用非const修饰的成员函数等 { if (!_dict.count(src)) { use_log(loglevel::DEBUG) << \"有用户进入到了翻译模块, client: [\" << client.ip() << \" : \" << client.port() << \"]# 查询 \" << src <None\"; return \"None\"; } auto iter = _dict.find(src); use_log(loglevel::DEBUG) << \"有用户进入到了翻译模块, client: [\" << client.ip() << \" : \" << client.port() << \"]# 查询 \" << src <\" <second; return iter->second; } ~dict() {}private: string _pathname; unordered_map _dict;};
dict.txt:
robot: 机器人flower: 花tree: 树table: 桌子chair: 椅子cup: 杯子bowl: 碗spoon: 勺子knife: 刀fork: 叉子music: 音乐movie: 电影game: 游戏park: 公园zoo: 动物园school: 学校hospital: 医院restaurant: 餐馆supermarket: 超市bank: 银行post office: 邮局library: 图书馆street: 街道road: 路mountain: 山river: 河流lake: 湖泊beach: 海滩cloud: 云rain: 雨;下雨snow: 雪;下雪wind: 风sun: 太阳moon: 月亮star: 星星sweet: 甜的sour: 酸的bitter: 苦的spicy: 辣的cold: 寒冷的;冷的hot: 炎热的;热的warm: 温暖的cool: 凉爽的big: 大的small: 小的long: 长的short: 短的;矮的fat: 胖的;肥的thin: 瘦的;薄的tall: 高的low: 低的fast: 快的;快速地slow: 慢的;缓慢地easy: 容易的difficult: 困难的beautiful: 美丽的ugly: 丑陋的kind: 善良的cruel: 残忍的clever: 聪明的stupid: 愚蠢的strong: 强壮的weak: 虚弱的open: 打开;开着的close: 关闭;关着的clean: 干净的;清洁dirty: 脏的new: 新的old: 旧的;老的young: 年轻的old-fashioned: 老式的;过时的modern: 现代的;时髦的expensive: 昂贵的cheap: 便宜的light: 轻的;灯heavy: 重的empty: 空的full: 满的remember: 记得;记住forget: 忘记begin: 开始end: 结束;结尾start: 开始;出发stop: 停止;阻止give: 给take: 拿;取;带走buy: 买sell: 卖 : 起飞bug: borrow: 借(入)lend: 借(出)arrive: 到达leave: 离开;留下find: 找到;发现lose: 丢失;失去dream: 梦想;做梦think: 思考;认为believe: 相信;认为doubt: 怀疑hope: 希望wish: 愿望;希望;祝愿
addr.hpp:
#pragma once#include #include #include #include #include #include using namespace std;class inetaddr{public: inetaddr(const sockaddr_in &addr) : _addr(addr) { _port = ntohs(_addr.sin_port); _ip = inet_ntoa(_addr.sin_addr); } string ip() { return _ip; } uint16_t port() { return _port; } ~inetaddr() {}private: sockaddr_in _addr; string _ip; uint16_t _port;};
udpclient.cc:
#include #include #include #include #include #include #include #include \"log.hpp\"using namespace std;int main(int argc, char *argv[]){ if (argc != 3) { cerr << \" please use: \" << argv[0] << \" server_ip server_port\" << std::endl; return 1; } // 创建套接字: int sd = socket(AF_INET, SOCK_DGRAM, 0); // 网络通信/用户数据报 if (sd < 0) use_log(loglevel::DEBUG) << \"socket failure!\"; use_log(loglevel::DEBUG) << \"socket success!\"; // os自己认识用户端机器的ip+随机会生成端口号port自动绑定与解除;故无需bind/程序终止立刻解除故查不到状态 string ip = argv[1]; // string内置机制不会直接拷贝指针 uint16_t port = stoi(argv[2]); // 先构造string //初始化des套接字: 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());//死循环式等待给服务端发送信息: while (1) { string mess; getline(cin, mess); socklen_t len = sizeof(local); //发送: int so = sendto(sd, mess.c_str(), mess.size(), 0, (sockaddr *)&local, len); // 输入型参数 if (so < 0) use_log(loglevel::DEBUG) << \"sendto failure!\"; //接收: char buff[1024] = {0}; ssize_t rm= recvfrom(sd, buff, sizeof(buff) - 1, 0, (sockaddr *)&local, &len); if (rm< 0) use_log(loglevel::DEBUG) << \"recvfrom failure!\"; cout<<\"查询结果是:\"<< buff<<endl;; }}
udpserver.cc:
#include\"udpserver.hpp\"#include \"addr.hpp\"#include\"dict.hpp\"// string echo_repeat(string mess,inetaddr addr){// string ret;// ret+=\"return \";// ret+=mess;// return ret;// }int main(int argc ,char* argv[]){ // if(argc != 3) // { // std::cerr << \"please use: \" << argv[0] << \" ip\"<<\" port\" << std::endl; // return 1; // } if(argc != 2) { std::cerr << \"please use: \" << argv[0] <<\" port\" << std::endl; return 1; } consolestrategy; //string ip=argv[1];//string内置机制不会直接拷贝指针 // uint16_t port=stoi(argv[2]);//先构造string uint16_t port=stoi(argv[1]); // unique_ptr ur=make_unique(ip,port); //unique_ptr ur=make_unique(port); //回调: //1`加载翻译词典: dict dt; dt.loaddict(); //2`服务端启动接受信息后进行查找功能: unique_ptr ur=make_unique(port,[&dt](string mess,inetaddr ar)->string{ return dt.translate(mess,ar); }); ur->init(); ur->start(); }
udpserver.hpp:
#include #include #include #include #include #include #include#include#include \"log.hpp\"#include \"addr.hpp\"using namespace std;const int delfd = -1;using func_t=function;// consolestrategy; 不允许全局直接使用操作;只允许定义声明等等class udpserver{public: // udpserver(string ip, uint16_t port) : _ip(ip), _port(port), // _isrunning(0), _socketfd(delfd) {} //这里服务器不能绑死否则只能接受指定的主机发来的信息了 //udpserver( uint16_t port) : _port(port), _isrunning(0), _socketfd(delfd) {} udpserver( uint16_t port,func_t func) : _port(port), _isrunning(0), _socketfd(delfd),_func(func) {} void init() { // 1`创建套接字: _socketfd = socket(AF_INET, SOCK_DGRAM, 0); // 网络通信/用户数据报;返回文件描述符 if (_socketfd < 0) use_log(loglevel::DEBUG) << \"socket failure!\"; use_log(loglevel::DEBUG) << \"socket success!\"; // 2`socket绑定信息: sockaddr_in local; // char sin_zero[8];填充字节,使 sockaddr_in 和 sockaddr 长度相同 //套接字结构体初始化: bzero(&local, sizeof(local)); local.sin_family = AF_INET;//网络通信 local.sin_port = htons(_port); // 把对应的16位转化成网络序列 // 内置的把string转化成大端类型的网络序列;也可手动操作(来回转化比较麻烦系统直接提供) // ntohs()网络序列转回本地 //. local.sin_addr.s_addr = inet_addr(_ip.c_str()); local.sin_addr.s_addr =INADDR_ANY ; // 或者直接输入0;可以理解成匹配同一主机的不同ip(多个网卡)的任意des-port相同的不同进程发来的信息 int n = bind(_socketfd, (sockaddr *)&local, sizeof(local));//程序终止bind的网络信息自动解除 if (n < 0) use_log(loglevel::DEBUG) << \"bind failure!\"; use_log(loglevel::DEBUG) <死循环: _isrunning = 1; while (_isrunning) { char buff[1024] = {0}; sockaddr_in per; socklen_t len = sizeof(per); // 套接字长度就是字节数 ssize_t rm = recvfrom(_socketfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&per, &len); // 输出型参数故取地址 if (rm< 0) use_log(loglevel::DEBUG) << \"recvfrom failure!\"; buff[rm]=0; string per_addr= inet_ntoa(per.sin_addr); uint16_t per_port=ntohs(per.sin_port); cout<< \"$client :[addr: \"<<per_addr<<\" port: \"<<per_port<<\" ] say: \"<<buff<<endl; //string res=\"server say:\"; // res+=buff; //int so=sendto(_socketfd,res.c_str(),res.size(),0,(sockaddr *)&per,len);//这里接收两个全都对套接字用的是指针:继承多态效果 //回调函数,最后传给client string ans= _func(buff,per); int so=sendto(_socketfd,ans.c_str(),ans.size(),0,(sockaddr *)&per,len);//这里接收两个全都对套接字用的是指针:继承多态效果 //输入型参数 if (so < 0) use_log(loglevel::DEBUG) << \"sendto failure!\"; } } ~udpserver() { }private: //string _ip; uint16_t _port; int _socketfd; bool _isrunning; func_t _func;};
间接包含部分:
就是上面展示的log.hpp/mutex.hpp/Makefile等等;这里就不展示了!!!
2.5基于UDP实现的server-clients通信改造多人聊天室(服务端多线程版本):
实现思路:
可能存在多个ip不同的用户端给服务端发信息;而服务端需要全部给这些连接服务端的用户端全部发送一遍(服务端收到的信息)--〉客户端任务:给服务端发信息;服务端任务:全部转给客户端一遍。妳因为客户端如果先recv再sendto;那么这里会阻塞住;因此效果不太好!
我们想要的是只有服务端收到信息就发给客户端-->因此客户端的接受和发送两个线程同时进行。而服务端一个线程来回处理的时候只能处理完一个发送任务再去按收,效率太慢了,因此改成线程池;但是对应这个route;它底层需要插入连按客户的addr;因此我们这里用的vector;临界资源-->线程不安全;加个锁就ok了(规定用户只要QUIT;然后服务端转发一遍;用用户端自动结束recv和sendto线程)! ! !
因此,我们把服务端多线程化去执行客户端发来的信息然后去群发(应用多线程);但是储存客户ip+port的数组是全局的(线程不安全)故还需加锁!!!【引入多线程就要考虑是否加锁问题】
下面我们 采用线程池来模拟多线程(但是用户上限有限制),群发调用回调方法进行发送:
效果展示:
当多个客户端进行连接通信的时候类似这样:
这里我们采用一个机器输入另一个窗口进行显示群发消息;因此又对客户端标准错误进行了重定向到 另一台机器!
客户端recvfrom处:
echo \"1\">/dev/pts/2//查询属于哪台机器
./udpclient 127.0.0.1 8080 2>/dev/pts/2//进行对应重定向
因此我们就可以采取上述方式进行执行客户端程序!
效果:
代码实现:
主要代码部分:
udpclient.cc:
#include #include #include #include #include #include #include #include \"log.hpp\"#include\"thread.hpp\"using namespace std;using namespace td;//先设置成全局的方便函数内使用string ip;uint16_t port;pthread_t id;//方便后序退出时候回收对应线程int flag=0;//标记用户QUIT后方便终止读与收两个线程int sd;void Recv(){ while(1){ char buff[1024] = {0}; sockaddr_in other; socklen_t len; ssize_t rm= recvfrom(sd, buff, sizeof(buff) - 1, 0, (sockaddr *)&other, &len); if (rm< 0) use_log(loglevel::DEBUG) << \"recvfrom failure!\"; buff[rm]=0; cerr<<buff<<endl;/////////////////方便后序重定向 }}void Send(){ 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()); socklen_t len = sizeof(local); string tip=\"我上线了 \"; int so = sendto(sd, tip.c_str(), tip.size(), 0, (sockaddr *)&local, len); //cout<<so<<endl; while (1) { //cout<<\"Please Enter#\"<<endl; string mess; getline(cin, mess); //发送: int so = sendto(sd, mess.c_str(), mess.size(), 0, (sockaddr *)&local, len); // 输入型参数 if (so < 0) use_log(loglevel::DEBUG) << \"sendto failure!\"; if(mess==\"QUIT\") { flag=1;//终止读线程 pthread_cancel(id); } }}int main(int argc, char *argv[]){ if (argc != 3) { cerr << \" please use: \" << argv[0] << \" server_ip server_port\" << std::endl; return 1; } // 创建套接字: sd = socket(AF_INET, SOCK_DGRAM, 0); // 网络通信/用户数据报 if (sd < 0) use_log(loglevel::DEBUG) << \"socket failure!\"; use_log(loglevel::DEBUG) << \"socket success!\"; // os自己认识用户端机器的ip+随机会生成端口号port自动绑定与解除;故无需bind/程序终止立刻解除故查不到状态 ip = argv[1]; port = stoi(argv[2]); //创建两个线程去执行接收和发送两个任务; Thread t1(Recv); Thread t2(Send); t1.start(); t2.start(); id=t2.Id(); t2.join(); if(flag==1) pthread_cancel(t1.Id()); t1.join(); }
udpserver.cc:
#include\"udpserver.hpp\"#include \"addr.hpp\"#include\"route.hpp\"#include\"threadpool.hpp\"#include// string echo_repeat(string mess,inetaddr addr){// string ret;// ret+=\"return \";// ret+=mess;// return ret;// }using fn=function;int main(int argc ,char* argv[]){ // if(argc != 3) // { // std::cerr << \"please use: \" << argv[0] << \" ip\"<<\" port\" << std::endl; // return 1; // } if(argc != 2) { std::cerr << \"please use: \" << argv[0] <<\" port\" << std::endl; return 1; } consolestrategy; //string ip=argv[1];//string内置机制不会直接拷贝指针 // uint16_t port=stoi(argv[2]);//先构造string uint16_t port=stoi(argv[1]); // unique_ptr ur=make_unique(ip,port); //unique_ptr ur=make_unique(port); //回调: Route r; //2`服务端启动接受信息后进行查找功能: //单线程:// unique_ptr ur=make_unique(port,[&r](int fd,string mess,inetaddr ar){// r.messroute(fd,mess,ar);// }); //线程池版本: auto tp=Threadpool::getinstance(); unique_ptr ur=make_unique(port,[&r,&tp](int fd, string mess,inetaddr ar){ //因此可以把messroute绑定成无参对象用bind: fn tk=std::bind(&Route::messroute,&r,fd,mess,ar);//此时就不用给线程池任务传参了;这里this指针必须给出值 tp->equeue(tk);//只允许传递无参数无返回值的对象或者函数进行处理}); ur->init(); ur->start(); }
udpserver.hpp:
#pragma once#include #include #include #include #include #include #include #include #include \"log.hpp\"#include \"addr.hpp\"using namespace std;const int delfd = -1;using func_t = function;// consolestrategy; 不允许全局直接使用操作;只允许定义声明等等class udpserver{public: // udpserver(string ip, uint16_t port) : _ip(ip), _port(port), // _isrunning(0), _socketfd(delfd) {} // 这里服务器不能绑死否则只能接受指定的主机发来的信息了 // udpserver( uint16_t port) : _port(port), _isrunning(0), _socketfd(delfd) {} udpserver(uint16_t port, func_t func) : _port(port), _isrunning(0), _socketfd(delfd), _func(func) {} void init() { // 1`创建套接字: _socketfd = socket(AF_INET, SOCK_DGRAM, 0); // 网络通信/用户数据报;返回文件描述符 if (_socketfd < 0) use_log(loglevel::DEBUG) << \"socket failure!\"; use_log(loglevel::DEBUG) << \"socket success!\"; // 2`socket绑定信息: sockaddr_in local; // char sin_zero[8];填充字节,使 sockaddr_in 和 sockaddr 长度相同 // 套接字结构体初始化: bzero(&local, sizeof(local)); local.sin_family = AF_INET; // 网络通信 local.sin_port = htons(_port); // 把对应的16位转化成网络序列 // 内置的把string转化成大端类型的网络序列;也可手动操作(来回转化比较麻烦系统直接提供) // ntohs()网络序列转回本地 //. local.sin_addr.s_addr = inet_addr(_ip.c_str()); local.sin_addr.s_addr = INADDR_ANY; // 或者直接输入0;可以理解成匹配同一主机的不同ip(多个网卡)的任意des-port相同的不同进程发来的信息 int n = bind(_socketfd, (sockaddr *)&local, sizeof(local)); // 程序终止bind的网络信息自动解除 if (n < 0) use_log(loglevel::DEBUG) << \"bind failure!\"; use_log(loglevel::DEBUG) <死循环: _isrunning = 1; while (_isrunning) { // cout<<\"再次准备\"<<endl; char buff[1024] = {0}; sockaddr_in per; socklen_t len = sizeof(per); // cout<<\"准备读信息\"<<endl; // 套接字长度就是字节数 ssize_t rm = recvfrom(_socketfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&per, &len); // 输出型参数故取地址 if (rm < 0) use_log(loglevel::DEBUG) << \"recvfrom failure!\"; buff[rm] = 0; string per_addr = inet_ntoa(per.sin_addr); uint16_t per_port = ntohs(per.sin_port); // cout<< \"$client :[addr: \"<<per_addr<<\" port: \"<<per_port<<\" ] say: \"<<buff<<endl; // string res=\"server say:\"; // res+=buff; // int so=sendto(_socketfd,res.c_str(),res.size(),0,(sockaddr *)&per,len);//这里接收两个全都对套接字用的是指针:继承多态效果 // 回调函数,最后传给聊天室所有用户: inetaddr ir(per); _func(_socketfd, buff, ir); } } ~udpserver() { }private: // string _ip; uint16_t _port; int _socketfd; bool _isrunning; func_t _func;};
addr.hpp:
#pragma once#include #include #include #include #include #include #includeusing namespace std;class inetaddr{public: // 网络序列转主机: inetaddr(sockaddr_in &addr) : _addr(addr) { _port = ntohs(_addr.sin_port); char buff[1024]; inet_ntop(AF_INET,&_addr.sin_addr,buff,sizeof(_addr)); _ip=buff; } // 客户端主机转网络: inetaddr(const string ip, uint16_t port) : _ip(ip), _port(port) { memset(&_addr, 0, sizeof(_addr)); _addr.sin_family = AF_INET; inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr); _addr.sin_port = htons(_port); } // 服务端主机转网络: inetaddr( uint16_t port) : _port(port) { memset(&_addr, 0, sizeof(_addr)); _addr.sin_family = AF_INET; inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr); _addr.sin_port = htons(_port); } sockaddr_in *addrptr() { return &_addr; } socklen_t addrlen() { return sizeof(_addr); } string ip() { return _ip; } uint16_t port() { return _port; } bool operator==(const inetaddr sockin) { return _ip == sockin._ip && _port == sockin._port; } sockaddr_in &sockaddr() { return _addr; } // 这里返回引用否则右值无地址可取(sendto) string get_userinfo() { return ip() + \" : \" + to_string(port()); } ~inetaddr() {}private: sockaddr_in _addr; string _ip; uint16_t _port;};
route.hpp:
#pragma once#include #include #include #include #include #include #include #include #include #include \"log.hpp\"#include \"addr.hpp\"using namespace std;class Route{private: bool is_exit(inetaddr ir){ for(int i=0;i<_peers.size();i++){ if(_peers[i]==ir) return 1; } return 0; } void addpeer(inetaddr ir){ _peers.push_back(ir); use_log(loglevel::DEBUG)<<\"成功添加一个用户:\"<<ir.get_userinfo(); } void deletepeer(inetaddr ir){ for(auto iter=_peers.begin();iter!=_peers.end();iter++ ){ if(*iter==ir){ _peers.erase(iter); use_log(loglevel::DEBUG)<<\"成功删除一个用户:\"<<ir.get_userinfo();} iter=_peers.end()-1;//迭代器失效 再使用会coredump // break ; } }public: Route() {} void messroute(int sockfd,string mess,inetaddr ir){ mutexguard md(_m);//多线程互斥;vector是共享的;stl不是线程安全的 if(!is_exit(ir)) addpeer(ir); string ans=\" [\"+ir.get_userinfo()+\"] say: \"+mess; for(auto&peer:_peers){ int so=sendto(sockfd,ans.c_str(),ans.size(),0,(sockaddr *)&(ir.sockaddr()),sizeof(ir.sockaddr())); if (so < 0) use_log(loglevel::DEBUG) << \"sendto failure!\"; } if(mess==\"QUIT\") { deletepeer(ir); } } ~Route() {}private: vector _peers; mutex _m;};
间接包含部分:
cond.hpp:
#pragma once#include #include #include #include #include #include #include #include \"mutex.hpp\"class cond{public: cond() { pthread_cond_init(&_cond, nullptr); } void Wait(mutex &mx){int n = pthread_cond_wait(&_cond, mx.getmutex());(void)n;} void notify() { int n = pthread_cond_signal(&_cond); (void)n; } void allnotify() { int n = pthread_cond_broadcast(&_cond); (void)n; } ~cond() { pthread_cond_destroy(&_cond); }private: pthread_cond_t _cond;};
thread.hpp:
#ifndef THREAD_H#define THREAD_H#include #include #include #include #include #include #include#include#includeusing namespace std;namespace td{ static uint32_t num=1; class Thread { using func_t = function; public: Thread(func_t func) : _tid(0), _res(nullptr), _func(func), _isrunning(false), _isdetach(false) { _name=\"Thread-\"+to_string(num++); } static void *Routine(void *arg){ Thread *self =static_cast(arg); //需要查看是否进行了start前的detach操作: pthread_setname_np(self->_tid, self->_name.c_str()); // cout<_name.c_str()<_isrunning=1; if(self->_isdetach) pthread_detach(self->_tid); self->_func(); return nullptr; } bool start(){ if(_isrunning) return false; int n = pthread_create(&_tid, nullptr, Routine, this); if (n != 0) { //cerr << \"create thread error: \" << strerror(n) << endl; return false; } else { //cout << _name << \" create success\" << endl; return true; } } bool stop(){ if(_isrunning){ int n= pthread_cancel(_tid); if (n != 0) { //cerr << \"cancel thread error: \" << strerror(n) << endl; return false; } else { _isrunning = false; // cout << _name << \" stop\" << endl; return true; } } return false; } bool detach(){ if(_isdetach) return false; if(_isrunning)pthread_detach(_tid);//创建成功的线程进行分离操作 _isdetach=1;//未创线程进行分离只进行标记 return true; } bool join(){ if(_isdetach) { // cout<<\"线程 \"<<_name<<\"已经被分离;不能进行join\"<<endl; return false; } //只考虑运行起来的线程了: int n = pthread_join(_tid, &_res); if (n != 0) { //std::cerr << \"join thread error: \" << strerror(n) << std::endl; } else { //std::cout << \"join success\" << std::endl; } return true; } pthread_t Id() {return _tid;} ~Thread() {} private: pthread_t _tid; string _name; void *_res; func_t _func; bool _isrunning; bool _isdetach; };}#endif
threadpoll.hpp:
#pragma once#include \"log.hpp\"#include \"cond.hpp\"#include \"thread.hpp\"using namespace td;const int N = 5;template class Threadpool{private: Threadpool(int num = N) : _size(num),_sleepingnums(0),_isrunning(0) { for (int i = 0; i handletask(); })); } } // 单例只允许实例出一个对象 Threadpool(const Threadpool &t) = delete; Threadpool &operator=(const Threadpool &t) = delete; void Start() { if (_isrunning)//勿忘标记位 return; _isrunning = true; for (int i = 0; i < _size; i++) { // use_log(loglevel::DEBUG) << \"成功启动一个线程\"; ; _threads[i].start(); } }public: static Threadpool *getinstance()//必须采用静态(不创建对象的前提下进行获得类指针) { if (_ins == nullptr) //双重判断-->假设一个线程很快完成单例化;然后后面的一群线程正好来了;如果没有双层判断;就会阻塞一个个发现不是空返回_ins; //非常慢;为了提高效率这样就不用加锁在一个个判断了还能保证线程安全。 { { mutexguard mg(_lock);//静态锁; if (_ins == nullptr) { _ins = new Threadpool(); use_log(loglevel::DEBUG) <Start();//创建单例自启动 } } } use_log(loglevel::DEBUG) << \"获得之前创建的一个单例\"; return _ins; } void stop()//不能立刻停止如果队列有任务还需要线程完成完然后从handl函数退出即可 { mutexguard mg(_Mutex);//这里为了防止多线程调用线程池但是单例化杜绝了这点 if (_isrunning) { _isrunning = 0;//因此只搞个标记 use_log(loglevel::DEBUG) <全部子线程都要退出 } return; } void join() { // mutexguard mg(_Mutex);这里不能加锁;如果join的主线程快的话;直接就拿到锁了 // 即使唤醒子线程;他们都拿不到锁故继续休眠等待锁;而主线程join这一直拿着 锁等子线程 // 故造成了---->死锁问题 // 但是可能出现多线程同时访问;后面把它设置单单例模式就好了 use_log(loglevel::DEBUG) << \"回收线程\"; for (int i = 0; i < _size; i++) _threads[i].join(); } bool equeue(const T &tk) { mutexguard mg(_Mutex); if (_isrunning) { _task.push(tk); if (_sleepingnums == _size) _Cond.notify(); // 全休眠必须唤醒一个执行 //use_log(loglevel::DEBUG) << \"成功插入一个任务并唤醒一个线程\"; return true; } return false; } void handletask() { // 类似popqueue char name[128];//在线程局部存储开;不用加锁非全局就行 pthread_getname_np(pthread_self(), name, sizeof(name)); while (1) { T t; { mutexguard gd(_Mutex); while (_task.empty() && _isrunning)//休眠条件 { _sleepingnums++; _Cond.Wait(_Mutex); _sleepingnums--; // cout<<1<<endl; } if (_task.empty() && !_isrunning)//醒来后发现符合退出条件就退出 { use_log(loglevel::DEBUG) << name << \"退出\"; break; // 代码块执行完了;锁自动释放 } t = _task.front(); _task.pop(); } t(); } } ~Threadpool() {}private: vector _threads; int _size; mutex _Mutex; cond _Cond; queue _task; bool _isrunning; int _sleepingnums; //仅仅只是声明 static Threadpool *_ins; static mutex _lock;};//类内声明内外定义初始化templateThreadpool*Threadpool ::_ins=nullptr;templatemutex Threadpool ::_lock;
剩下的就是mutex/log.hpp见上面代码展示;这里就不重复了!
优化关于网络序列和本机序列之间的转化:
之前对port的网络本机序列转化:
uint16 t per port=ntohs(per.sin_port)
local.sin port = htons( port);
之前对ip的网络本机序列转化:
local.sin addr.s addr = inet addr( ip.c str());
string per addr= inet ntoa(per.sin_addr);
直接上结论:
对port的操作可以不用变;但是对于ip此时就要更换了;
原因:
对于这个ip之间转换是不安全的;也就是存存储在静态区的块地址;当多次调用它会被覆盖掉!此外由于静态区也不是线程安全的!
虽然不需要手动释放内存(new);man 手册上说,inet_ntoa 函数,是把这个返回结果放到了静态存储区.这个时候不需要我们手动进行释放;但是每次调用这个函数它都会从原来的位置进行覆盖!
看一下效果:
明显就出现问题了!!!
因此对于ip的转换我们引入了新的函数(这里p就代表进程):
#include int inet_pton(int af, const char *src, void *dst);
#include const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
结合例子解释下:
因此总结下当我们想port的网络本机之间转化就用ntohs/htons;当ip的本机与网络转换就用inet_pton/inet_ntop!
这俩函数我们在上面实现聊天室的时候对于封装的addr.hpp内就重新应用了!!!
其次就是对于服务端;也就是server.hpp我们是不允许拷贝等等的;因此需要禁用掉类似的接口-->此时我们采取继承的方式;此时构建server的类的时候需要先构建它继承的基类;发现基类被禁用了直接报错-->后续实现tcp的时候我们再采用!
三·常用的网络指令:
3.1ifconfig:
使用该指令进行查看配置信息:
上面的是子网ip;下面的是本地环回ip!
本地环回:要求c、s必须在一台机器上,表明我们是本地通信,client发送的数据,不会被推送到网络而是在OS内部,转一圈直接交给对应的服务器端;如果被通信的端绑定的是这个环回地址:
再进行通信那么就是本地通信了;不会发送到网络;经常用来进行网络代码的测试!
子网ip:由本地局域网(LAN)自动分配给连接的设备的ip!(我们进行本地实现udp通信就是拿它或者本地环回作为目的ip进行连接服务端的)
3.2netstat:
netstat 是一个用来查看网络状态的重要工具!
用法:netstat+ -【选项】
选项:
n 拒绝显示别名, 能显示数字的全部转化成数字l 仅列出有在 Listen (监听) 的服務状态
p 显示建立相关链接的程序名
t (tcp)仅显示 tcp 相关选项
u (udp)仅显示 udp 相关选项
a (all)显示所有选项, 默认不显示 LISTEN 相关->之后我们的TCP通信会用到!
下面我们来演示下效果(选项谁在前谁在后无影响) :
根据需要进行显示:
程序终止后自动解除这个信息:
3.3ping:
使用 ping 工具来测试本地计算机与服务器之间的网络连接情况。
下面我们测试一下:
ping www.qq.com
这里了解下即可!!!
3.4pidof:
在查看服务器的进程 id 时非常方便!
用法 :pidof [进程名]来查看对应pid
四·本篇小结:
通过本篇文章;在有网络概念的基础上;来更清楚认识网络通信是如何进行的,关键(ip+port->socket);之后又认识了相关socket相关地址结构体底层结构,简单协议介绍以及一些网络通信(udp)的一些api接口后来基于UDP网络通信实现的简单的server-client之间的应答/词典翻译/多人聊天室等小项目最后补充了点相关小指令;博主学习这块也是用了好几天;然后整理笔记;最近有时间复盘一下整理的博客;欢迎大家阅读;下期找时间更新TCP网络通信!!!