网络编程套接字(2): 简单的UDP网络程序

文章目录

  • 网络编程套接字(2): 简单的UDP网络程序
    • 3. 简单的UDP网络程序
      • 3.1 服务端创建
        • (1) 创建套接字
        • (2) 绑定端口号
        • (3) sockaddr_in结构体
        • (4) 数据的接收与发送
          • 接收
          • 发送
      • 3.2 客户端创建
      • 3.3 代码编写
        • (1) v1_简单发送消息
        • (2) v2_小写转大写
        • (3) v3_模拟命令行解释器
        • (4) v4_多线程版本的群聊系统
        • (5) v5_Windows与Linux配合聊天室

网络编程套接字(2): 简单的UDP网络程序

3. 简单的UDP网络程序

3.1 服务端创建

(1) 创建套接字

在这里插入图片描述

create an endpoint for communication: 创建用于通信的端点

头文件:#include <sys/types.h>         #include <sys/socket.h>函数原型:int socket(int domain, int type, int protocol);参数说明:第一个参数domain:   指定套接字的通信域第二个参数type:     指定套接字的服务类型(套接字的种类) 第三个参数protocol: 代表创建套接字的协议(默认为0),0,系统会自动判断是tcp还是udp返回值: 套接字创建成功: 返回一个文件描述符套接字创建失败: 返回-1, 并且设置错误码

关于socket参数详细介绍:

(1) domain: 指定套接字的通信域,相当于 struct sockaddr结构体的前16比特位(2字节)

在这里插入图片描述

domain的选项是以宏的形式给出的,我们直接选用即可。常用就是上面框住的两个:

  • AF_UNIX,本地通信
  • AF_INET(IPv4)或者 AF_INET6(IPv6),网络通信

(2) type: 指定套接字的服务类型

在这里插入图片描述

该参数的选项也是像domain一样以宏的形式给出,直接选用。常用的是上面两个:

  • SOCK_STREAM: 基于TCP的网络通信,流式套接字,提供的是流式服务(对应TCP的特点:面向字节流)

  • SOCK_DGRAM: 基于UDP的网络通信,套接字数据报,提供的用户数据报服务(对应UDP的特点:面向数据报)

(2) 绑定端口号

在这里插入图片描述

bind a name to socket:将名称绑定到套接字

头文件:#include <sys/types.h>          #include <sys/socket.h>函数原型:int bind(int sockfd, const struct sockaddr *addr,  socklen_t addrlen);参数说明:第一个参数sockfd:  文件描述符, 即要绑定的套接字第二个参数addr:    网络相关的结构体, 包含IP地址、端口号等第三个参数addrlen: 传入结构体addr(第二个参数)的实际长度大小返回值:绑定成功: 返回0绑定失败: 返回-1,并且设置错误码

参数addr的类型是:struct sockaddr *,也就是如图的结构体

在这里插入图片描述

我们需要做的就是:定义一个 sockaddr_in 的结构体,即上图的第二个结构体,然后对该结构体进行内容填充,填完就把给结构体传给第二个参数addr,需要强制类型转换

(3) sockaddr_in结构体

在这里插入图片描述

  • __SOCKADDR_COMMON是一个宏

在这里插入图片描述

#define	__SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family

sa_prefix:代表外面传入的参数sin_

sa_prefix##family:##代表合并拼接,意思是sa_prefix与family合并拼接

sa_prefix就是sin_,则sa_prefix##family表示sin_family

sa_family_t:代表16位整数

在这里插入图片描述

就是这16位地址类型

在这里插入图片描述

  • sin_port: 是当前服务器需要绑定的端口号,它的类型是 in_port_t,代表16位的整数

在这里插入图片描述

  • sin_addr: 代表IP地址,它的类型是一个in_addr的结构体,它里面的内容是32位的整数

在这里插入图片描述

  1. 关于这个IP地址:我们要传入字符串风格的,但是这里需要4字节整数风格,所以需要转化,比如"1.1.1.1"-> uint32_t,问:能不能强转呢?
    不能强转, 强转只能改变类型, 不改变二进制构成

  2. 我们转化完了还是本主机的4字节序列,需要网络序列,所以要将主机序列转化成为网络序列

上面的2步用 inet_addr函数就可以完成

在这里插入图片描述

  1. 但是我们的云服务器,或者一款服务器,一般不要指明某一个确定的IP

所以这里的ip地址我们填 INADDR_ANY,这是一个宏,代表 0.0.0.0,叫做任意地址绑定

  • sin_zero: 表示该结构体的填充字段(即上面讲的sin_family,sin_port,sin_addr.s_add)

总结: 未来使用这个函数时,需要所以填充:sin_family,sin_port,sin_addr.s_addr这3个字段,因为不关注其他字段,所以在填充之前需要对该结构体清空,我们可以采用 memset或 bzero函数来完成。

bind的作用:

上面如果我们只设置了sockaddr_in这个结构体,它只是在用户空间的特定函数栈帧上,不在内核中,所以bind的作用就是把文件字段进行绑定关联,这样这个文件就是网络文件

(4) 数据的接收与发送

接收

在这里插入图片描述

receive a message from a socket:从套接字接收消息

头文件: #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);参数说明:第一个参数sockfd: 文件描述符,就是上面我们绑定好的套接字第二个参数buf: 接收数据的缓冲区(自己定义)第三参数len:   缓冲区的长度第四个参数flags: 读取方式,默认设为0表示阻塞式读取第五个参数src_addr: (输入)对应套接字的接收缓冲区第六个参数addrlen:(输出)src_addr结构体的长度返回值:成功: 返回实际读到的字节数失败: 失败返回-1,并设置错误码

socklen_t 是一个32位的无符号整数

  • 参数src_addr与addrlen:输入输出型参数

  • src_addr: 输入时传入对应套接字的接收缓冲区,输出时包含客户端的ip和port

  • addrlen: 输入时传入对应套接字的接收缓冲区,输出时表示实际输出的结构体大小

我们做的是定义一个 sockaddr_in 的结构体,把结构体传给参数src_addr,需要强制类型转换

发送

在这里插入图片描述

send a message on a socket: 在套接字上发送消息

头文件:#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);
参数说明:第一个参数sockfd: 文件描述符,从哪个套接字去发送消息第二个参数buf: 发送数据的缓冲区第三参数len: 要发送的数据长度第四个参数flags:发送方式,默认设为0表示阻塞式发送第五个参数dest_addr:下面解释第六个参数addrlen:dest_addr结构体的长度返回值:成功: 实际发送的字节数失败: 失败返回-1,并设置错误码
  • dest_addr和addrlen 是一个输入型参数

  • dest_addr:指向目的地址的结构体指针,表示要发给谁

  • addrlen:表示目的地址结构体的长度

我们做的是定义一个 sockaddr_in 的结构体,然后对该结构体进行内容填充,填完就把给结构体传给dest_addr**,需要强制类型转换**

3.2 客户端创建

还是3步:创建套接字,bind(不需要自己绑定,由OS自动分配),处理数据接收与发送

3.3 代码编写

这里一共提供5个版本的udp代码

err.hpp:这个代码是公用的后续不在给出

#pragma once
enum
{USAGE_ERR=1,SOCKET_ERR,BIND_ERR,
};

(1) v1_简单发送消息

客户端向服务端发送消息,服务端收到后再把消息发回给客户端

udp_server.hpp

#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include"err.hpp"
using namespace std;namespace ns_server
{const static uint16_t default_port=8080;class Udpserver{public:Udpserver(uint16_t port=default_port):port_(port){cout<<"server addr: "<<port_<<endl;}void InitServer(){// 1. 创建socket接口, 打开网络文件(本质)sock_=socket(AF_INET,SOCK_DGRAM,0);if(sock_<0){cerr<<"create socket error: "<<strerror(errno)<<endl;exit(SOCKET_ERR);}cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3// 2. 给服务器指明IP地址和Port端口号// 填充一下服务器的IP和Portstruct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!// 清空localbzero(&local,sizeof(local));  // 用memset也可以// 填充sockaddr_in结构local.sin_family=AF_INET;local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列// 使用 inet_addr就可以做下面两件事情:// (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成// (2) 需要将主机序列转化成为网络序列// (3)云服务器,或者一款服务器,一般不要指明某一个确定的IPlocal.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    // 把套接字字段和文件字段进行关联  --- 网络文件int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    if(n<0){cerr<<"bind socket error: "<<strerror(errno)<<endl;exit(BIND_ERR);}cout<<"bind socket success: "<<sock_<<endl;   }void Start(){char buffer[1024];       // 保存用户数据的缓冲区while(true){// 收到来自客户端发送的消息struct sockaddr_in peer;          // 远端socklen_t len = sizeof(peer);     // 这里一定要写清楚, 未来你传入的缓冲区大小// 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  if(n>0)    // 读取数据成功buffer[n]='\0';elsecontinue;// 提取client信息   --- debugstring clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;// 把消息发给别人// 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定// 谁给我发的, 我就把消息转给谁// peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行sendto(sock_,buffer,strlen(buffer),0,(struct sockaddr*)&peer, 		  	                        sizeof(peer));}}~Udpserver(){}private:int sock_;         //套接字(文件描述符)uint16_t port_;    //端口号(本地主机序列构建的port)};
}

udp_server.cc

#include"udp_server.hpp"
using namespace ns_server;// 运行格式: ./udp_server port// 使用手册
static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}int main(int argc,char*argv[])
{if(argc != 2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型unique_ptr<Udpserver> usvr(new Udpserver(port));usvr->InitServer();     // 服务器初始化usvr->Start();return 0;
}

udp_client.cc

#pragma once#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}// udp_client serverip serverport
int main(int argc,char*argv[])
{if(argc !=3){usage(argv[0]);exit(USAGE_ERR);}// 拿到服务端的ip和端口号string serverip=argv[1];uint16_t serverport=atoi(argv[2]);// 1. 创建套接字int sock=socket(AF_INET,SOCK_DGRAM,0);if (sock < 0){cerr << "create socket error: " << strerror(errno) << endl;exit(SOCKET_ERR);}// 2. 关于客户端的绑定// client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip:        serverport]// 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?// client的port要随机让OS分配防止client出现启动冲突 // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化// 明确服务器是谁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());// 3. 向服务器发送消息(这里暂时由用户充当)while(true){// 用户输入string message;cout<< "please Enter# ";cin>>message;// 什么时候bind呢?// 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建发送的数据报文// 发送sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server,                        sizeof(server));// 把消息再收回来(回显回来)char buffer[1024];struct sockaddr_in temp;socklen_t len =sizeof(temp);int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);if(n>0){buffer[n]='\0';cout<<"server echo# "<<buffer<<endl;}}
}

运行结果:

先运行服务端,再启动客户端,客户端先用本地环回进行测试,测试成功

在这里插入图片描述

运行程序后看到套接字是创建成功的,对应得到到的文件描述符是3,这也很好理解,因为0、1、2默认被标准输入流、标准输出流和标准错误流占用了,此时最小的、未被使用用的文件描述符就是3

(2) v2_小写转大写

v2在v1版本的基础增加了业务处理,上层使用了回调函数实现大小写转换

udp_server.hpp

#pragma once#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
using namespace std;namespace ns_server
{const static uint16_t default_port=8080;using func_t =function<string(string)>;    //这是一个函数class Udpserver{public:Udpserver(func_t cb, uint16_t port=default_port):service_(cb),port_(port){cout<<"server addr: "<<port_<<endl;}void InitServer(){// 1. 创建socket接口, 打开网络文件(本质)sock_=socket(AF_INET,SOCK_DGRAM,0);if(sock_<0){cerr<<"create socket error: "<<strerror(errno)<<endl;exit(SOCKET_ERR);}cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3// 2. 给服务器指明IP地址和Port端口号// 填充一下服务器的IP和Portstruct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!// 清空localbzero(&local,sizeof(local));  // 用memset也可以// 填充sockaddr_in结构local.sin_family=AF_INET;local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列// 使用 inet_addr就可以做下面两件事情:// (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成// (2) 需要将主机序列转化成为网络序列// (3)云服务器,或者一款服务器,一般不要指明某一个确定的IPlocal.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    // 把套接字字段和文件字段进行关联  --- 网络文件int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    if(n<0){cerr<<"bind socket error: "<<strerror(errno)<<endl;exit(BIND_ERR);}cout<<"bind socket success: "<<sock_<<endl;   }void Start(){char buffer[1024];       // 保存用户数据的缓冲区while(true){// 收到来自客户端发送的消息struct sockaddr_in peer;                 // 远端socklen_t len = sizeof(peer);             // 这里一定要写清楚, 未来你传入的缓冲区大小// 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  if(n>0)    // 读取数据成功buffer[n]='\0';elsecontinue;// 提取client信息   --- debugstring clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;// 做业务处理string response=service_(buffer);// 把消息发给别人// 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定// 谁给我发的, 我就把消息转给谁// peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行sendto(sock_,response.c_str(),response.size(),0,(struct sockaddr*)&peer, 						sizeof(peer));}}~Udpserver(){}private:int sock_;         //套接字(文件描述符)uint16_t port_;    //端口号(本地主机序列构建的port)func_t service_;    //我们的网络服务器刚刚解决的是网络IO的问题, 要进行业务处理(一个类内的回调方法)};
}

udp_server.cc

#include"udp_server.hpp"
using namespace ns_server;// 运行格式: ./udp_server port// 使用手册
static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}// 上层的业务处理, 不关心网络发送, 只负责信息处理即可// 这里是小写转大写
string transactionString(string request)   // request就是一个字符串
{string ret;char c;for(auto&r:request){if(islower(r)){c=toupper(r);ret.push_back(c);}else{ret.push_back(r);}}return ret;
}
int main(int argc,char*argv[])
{if(argc != 2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型unique_ptr<Udpserver> usvr(new Udpserver(transactionString,port));usvr->InitServer();     // 服务器初始化usvr->Start();return 0;
}

udp_client.cc

#pragma once#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}// udp_client serverip serverport
int main(int argc,char*argv[])
{if(argc !=3){usage(argv[0]);exit(USAGE_ERR);}// 拿到服务端的ip和端口号string serverip=argv[1];uint16_t serverport=atoi(argv[2]);// 1. 创建套接字int sock=socket(AF_INET,SOCK_DGRAM,0);if (sock < 0){cerr << "create socket error: " << strerror(errno) << endl;exit(SOCKET_ERR);}// 2. 关于客户端的绑定// client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip:        		serverport]// 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?// client的port要随机让OS分配防止client出现启动冲突 // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化// 明确服务器是谁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());// 3. 向服务器发送消息(这里暂时由用户充当)while(true){// 用户输入string message;cout<< "please Enter# ";cin>>message;// 什么时候bind呢?// 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建发送的数据报文// 发送sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server,                        sizeof(server));// 把消息再收回来(回显回来)char buffer[1024];struct sockaddr_in temp;socklen_t len =sizeof(temp);int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);if(n>0){buffer[n]='\0';cout<<"server echo# "<<buffer<<endl;}}
}

运行结果:

在这里插入图片描述

(3) v3_模拟命令行解释器

v3是在v2原有的业务处理下修改了功能,只要我们在客户端输入命令服务端就会返回运行结果,popen函数可以实现简单的命令行解释

udp_server.hpp

#pragma once#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
using namespace std;namespace ns_server
{const static uint16_t default_port=8080;using func_t =function<string(string)>;    //这是一个函数class Udpserver{public:Udpserver(func_t cb, uint16_t port=default_port):service_(cb),port_(port){cout<<"server addr: "<<port_<<endl;}void InitServer(){// 1. 创建socket接口, 打开网络文件(本质)sock_=socket(AF_INET,SOCK_DGRAM,0);if(sock_<0){cerr<<"create socket error: "<<strerror(errno)<<endl;exit(SOCKET_ERR);}cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3// 2. 给服务器指明IP地址和Port端口号// 填充一下服务器的IP和Portstruct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!// 清空localbzero(&local,sizeof(local));  // 用memset也可以// 填充sockaddr_in结构local.sin_family=AF_INET;local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列// 使用 inet_addr就可以做下面两件事情:// (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成// (2) 需要将主机序列转化成为网络序列// (3)云服务器,或者一款服务器,一般不要指明某一个确定的IPlocal.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    // 把套接字字段和文件字段进行关联  --- 网络文件int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    if(n<0){cerr<<"bind socket error: "<<strerror(errno)<<endl;exit(BIND_ERR);}cout<<"bind socket success: "<<sock_<<endl;   }void Start(){char buffer[1024];       // 保存用户数据的缓冲区while(true){// 收到来自客户端发送的消息struct sockaddr_in peer;                 // 远端socklen_t len = sizeof(peer);             // 这里一定要写清楚, 未来你传入的缓冲区大小// 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  if(n>0)    // 读取数据成功buffer[n]='\0';elsecontinue;// 提取client信息   --- debugstring clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;// 做业务处理string response=service_(buffer);// 把消息发给别人// 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定// 谁给我发的, 我就把消息转给谁// peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行sendto(sock_,response.c_str(),response.size(),0,(struct sockaddr*)&peer, 						sizeof(peer));}}~Udpserver(){}private:int sock_;         //套接字(文件描述符)uint16_t port_;    //端口号(本地主机序列构建的port)func_t service_;    //我们的网络服务器刚刚解决的是网络IO的问题, 要进行业务处理(一个类内的回调方法)};
}

udp_server.cc

#include"udp_server.hpp"
using namespace ns_server;// 运行格式: ./udp_server port// 使用手册
static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}// 上层的业务处理, 不关心网络发送, 只负责信息处理即可
static bool isPass(string &command)
{auto pos=command.find("rm");if(pos!=string::npos) return false;pos=command.find("mv");if(pos!=string::npos) return false;pos=command.find("while");if(pos!=string::npos) return false;pos=command.find("kill");if(pos!=string::npos) return false;return true;
}// 让同学们, 在你的本地把命令给我, server再把结果给你!
string excuteCommand(string command)   // command就是一个命令
{// 1. 安全检查if(!isPass(command))return "you are a bad man";// 2. 业务逻辑处理FILE*fp=popen(command.c_str(),"r");if(fp==nullptr)return "None";// 3. 获取结果char line[1024];string ret;while(fgets(line,sizeof(line),fp)!=NULL){ret+=line;}pclose(fp);return ret;
}int main(int argc,char*argv[])
{if(argc != 2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型unique_ptr<Udpserver> usvr(new Udpserver(excuteCommand,port));usvr->InitServer();     // 服务器初始化usvr->Start();return 0;
}

udp_server.cc

#pragma once#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}// udp_client serverip serverport
int main(int argc,char*argv[])
{if(argc !=3){usage(argv[0]);exit(USAGE_ERR);}// 拿到服务端的ip和端口号string serverip=argv[1];uint16_t serverport=atoi(argv[2]);// 1. 创建套接字int sock=socket(AF_INET,SOCK_DGRAM,0);if (sock < 0){cerr << "create socket error: " << strerror(errno) << endl;exit(SOCKET_ERR);}// 2. 关于客户端的绑定// client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip: serverport]// 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?// client的port要随机让OS分配防止client出现启动冲突 // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化// 明确服务器是谁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());// 3. 向服务器发送消息(这里暂时由用户充当)while(true){// 用户输入string message;cout<< "[遇健的服务器]# ";getline(cin,message);// 什么时候bind呢?// 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建            发送的数据报文// 发送sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server,                       sizeof(server));// 把消息再收回来(回显回来)char buffer[2048];struct sockaddr_in temp;socklen_t len =sizeof(temp);int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);if(n>0){buffer[n]='\0';cout<<"server echo# "<<buffer<<endl;}}
}

运行结果:

在这里插入图片描述

(4) v4_多线程版本的群聊系统

v4在v3的基础上加入了之前写的生产消费者模型,多线程实现了群聊系统

LockGuard.hpp

#include<iostream>
#include<pthread.h>
using namespace std;class Mutex   //自己不维护锁,由外部传入
{
public:Mutex(pthread_mutex_t* mutex):_pmutex(mutex){}void lock(){pthread_mutex_lock(_pmutex);}void unlock(){pthread_mutex_unlock(_pmutex);}~Mutex(){}private:pthread_mutex_t* _pmutex;   //锁的指针
};class LockGuard  //自己不维护锁,由外部传入
{
public:LockGuard(pthread_mutex_t* mutex):_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}private:Mutex _mutex;   //锁的指针
};

RingQueue.hpp

#include<iostream>
#include<pthread.h>
#include<vector>
#include<time.h>
#include<sys/types.h>
#include<unistd.h>
#include<semaphore.h>
#include<mutex>
using namespace std;// 生产者和消费者要有自己的下标来表征生产和消费要访问哪个资源
static const int N=5;template<class T>
class RingQueue
{
private:void P(sem_t &s){sem_wait(&s);}void V(sem_t &s){sem_post(&s);}void Lock(pthread_mutex_t &m){pthread_mutex_lock(&m);}void Unlock(pthread_mutex_t &m){pthread_mutex_unlock(&m);}public:RingQueue(int num=N):_ring(num),_cap(num){sem_init(&_data_sem,0,0);sem_init(&_space_sem,0,num);_c_step=_p_step=0;pthread_mutex_init(&_c_mutex,nullptr);pthread_mutex_init(&_p_mutex,nullptr);}void push(const T&in)     // 对应生产者{// 1.信号量的好处:// 可以不用在临界区内部做判断, 就可以知道临界资源的使用情况// 2.什么时候用锁, 什么时候用sem? --- 你对应的临界资源, 是否被整体使用!// 生产 --- 先要申请信号量// 信号量申请成功 - 则一定能访问临界资源P(_space_sem);Lock(_p_mutex);// 一定要有对应的空间资源给我!不用做判断, 是哪一个资源给生产者呢_ring[_p_step++]=in;_p_step%=_cap;V(_data_sem);Unlock(_p_mutex);}void pop(T*out)           // 对应消费者{// 消费P(_data_sem);    // 1.   先申请信号量是为了更高效Lock(_c_mutex);  // 2. *out=_ring[_c_step++];_c_step%=_cap;V(_space_sem);Unlock(_c_mutex);}~RingQueue(){sem_destroy(&_data_sem);sem_destroy(&_space_sem);pthread_mutex_destroy(&_c_mutex);pthread_mutex_destroy(&_p_mutex);}
private:vector<T> _ring;int _cap;           // 环形队列容器大小sem_t _data_sem;    // 只有消费者关心sem_t _space_sem;   // 只有生产者关心int _c_step;        // 消费位置int _p_step;        // 生产位置pthread_mutex_t _c_mutex;pthread_mutex_t _p_mutex;
};

udp_server.hpp

#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
#include<unordered_map>
#include"ringQueue1.hpp"
#include"lockGuard.hpp"
#include"thread.hpp"
using namespace std;// 群聊系统 --- 一个线程收消息, 一个线程发消息
// 标识一个客户端: ip+port , 使用unordered_map构建<ip+port, 客户端套接字>来表示某个用户发的消息namespace ns_server
{const static uint16_t default_port=8080;using func_t =function<string(string)>;    //这是一个函数class Udpserver{public:Udpserver(uint16_t port=default_port):port_(port){cout<<"server addr: "<<port_<<endl;pthread_mutex_init(&_lock,nullptr);p=new Thread(1,bind(&Udpserver::Recv,this));c=new Thread(2,bind(&Udpserver::Broadcast,this));}void StartServer(){// 1. 创建socket接口, 打开网络文件(本质)sock_=socket(AF_INET,SOCK_DGRAM,0);if(sock_<0){cerr<<"create socket error: "<<strerror(errno)<<endl;exit(SOCKET_ERR);}cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3// 2. 给服务器指明IP地址和Port端口号// 填充一下服务器的IP和Portstruct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!// 清空localbzero(&local,sizeof(local));  // 用memset也可以// 填充sockaddr_in结构local.sin_family=AF_INET;local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列// 使用 inet_addr就可以做下面两件事情:// (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成// (2) 需要将主机序列转化成为网络序列// (3)云服务器,或者一款服务器,一般不要指明某一个确定的IPlocal.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    // 把套接字字段和文件字段进行关联  --- 网络文件int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    if(n<0){cerr<<"bind socket error: "<<strerror(errno)<<endl;exit(BIND_ERR);}cout<<"bind socket success: "<<sock_<<endl;   p->run();c->run();}void addUser(const string&name,const struct sockaddr_in&peer){// online[name]=peerLockGuard lockguard(&_lock);auto iter=onlineuser.find(name);if(iter!=onlineuser.end())        // 存在(找到了)直接返回return;onlineuser.insert(make_pair(name,peer));   // 不存在(没找到)就插入}void Recv(){char buffer[1024];       // 保存用户数据的缓冲区while(true){// 收到来自客户端发送的消息struct sockaddr_in peer;                 // 远端socklen_t len = sizeof(peer);             // 这里一定要写清楚, 未来你传入的缓冲区大小// 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  if(n>0)    // 读取数据成功buffer[n]='\0';elsecontinue;// 提取client信息   --- debugstring clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;// 构建一个用户, 并检查string name=clientip;name+="-";name+=to_string(clientport);// 构建哈希表来存储用户 - 如果不存在,就插入;如果存在,什么都不做addUser(name,peer);  string message=name+">>"+buffer;  _rq.push(message);    // 消息放入环形队列中}}// 发消息  --- 给所有在线用户void Broadcast(){while(true){string sendstring;_rq.pop(&sendstring);    // 从环形队列中读到了消息   vector<struct sockaddr_in> v;   // 把需要发送的信息放到(拷贝)一个数组中<这是内存级的拷贝>{LockGuard lockguard(&_lock);for (auto user:onlineuser){v.push_back(user.second);}}for(auto user: v){sendto(sock_,sendstring.c_str(),sendstring.size(),0,(struct sockaddr*)&user,sizeof(user));cout<<"send done ..."<<sendstring<<endl;}}}~Udpserver(){pthread_mutex_destroy(&_lock);p->join();c->join();delete p;delete c;}private:int sock_;         //套接字(文件描述符)uint16_t port_;    //端口号(本地主机序列构建的port)unordered_map<string, struct sockaddr_in> onlineuser;   // 保存在线用户 --- 需要加锁保证安全pthread_mutex_t _lock;RingQueue<string> _rq;Thread*p;Thread*c;};
}

udp_server.cc

#include"udp_server.hpp"
using namespace ns_server;// 运行格式: ./udp_server port// 使用手册
static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}// 上层的业务处理, 不关心网络发送, 只负责信息处理即可int main(int argc,char*argv[])
{if(argc != 2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型unique_ptr<Udpserver> usvr(new Udpserver(port));usvr->StartServer();return 0;
}

udp_client.cc

#pragma once#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}// udp_client serverip serverport
int main(int argc,char*argv[])
{if(argc !=3){usage(argv[0]);exit(USAGE_ERR);}// 拿到服务端的ip和端口号string serverip=argv[1];uint16_t serverport=atoi(argv[2]);// 1. 创建套接字int sock=socket(AF_INET,SOCK_DGRAM,0);if (sock < 0){cerr << "create socket error: " << strerror(errno) << endl;exit(SOCKET_ERR);}// 2. 关于客户端的绑定// client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip: serverport]// 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?// client的port要随机让OS分配防止client出现启动冲突 // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化// 明确服务器是谁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());// 3. 向服务器发送消息(这里暂时由用户充当)while(true){// 用户输入string message;cout<< "[遇健的服务器]# ";getline(cin,message);// 什么时候bind呢?// 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建发送的数据报文// 发送sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server,                        sizeof(server));// 把消息再收回来(回显回来)char buffer[2048];struct sockaddr_in temp;socklen_t len =sizeof(temp);int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);if(n>0){buffer[n]='\0';cout<<"server echo# "<<buffer<<endl;}}
}

运行结果:

在这里插入图片描述

在这里插入图片描述

(5) v5_Windows与Linux配合聊天室

我们可以以Linux云服务器作为服务端,Windows作为客户端,在Windows下我们要修改成Windows下的接口,同时开放云服务器的端口号,使用v4版本的服务端代码

Windows下的客户端

#define _CRT_SECURE_NO_WARNINGS#include<iostream>
#include<WinSock2.h>
#include<string>
#include<cstring>
using namespace std;#pragma warning(disable:4996)
#pragma comment(lib,"ws2_32.lib")uint16_t serverport = 8080;
std::string serverip = "47.108.235.67";//std::string serverip = "127.0.0.1";int main()
{WSADATA WSAData;if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0){cerr << "init error" << endl;return -1;}SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0){cerr << "create socket error: " << strerror(errno) << endl;exit(-2);}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());// 3. 向服务器发送消息(这里暂时由用户充当)while (true){// 用户输入string message;cout << "Please Enter Your Message# ";getline(cin, message);// 发送sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server,                      sizeof(server));char buffer[2048];struct sockaddr_in temp;int len = sizeof(temp);int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp,                           &len);if (n > 0){buffer[n] = '\0';cout << buffer << endl;     // 往1号文件描述符输出}}closesocket(sock);WSACleanup();return 0;
}

运行结果:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/107217.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[论文阅读笔记26]Tracking Everything Everywhere All at Once

论文地址: 论文 代码地址: 代码 这是一篇效果极好的像素级跟踪的文章, 发表在ICCV2023, 可以非常好的应对遮挡等情形, 其根本的方法在于将2D点投影到一个伪3D(quasi-3D)空间, 然后再映射回去, 就可以在其他帧中得到稳定跟踪. 这篇文章的方法不是很好理解, 代码也刚开源, 做一…

兵力集中更容易进攻获胜

我兵力集中&#xff0c;敌兵力分散&#xff0c;进攻可胜 【安志强趣讲《孙子兵法》第21讲】 【原文】 进而不可御者&#xff0c;冲其虚也&#xff1b;退而不可追者&#xff0c;速而不可及也。 【趣讲白话】 进攻时&#xff0c;敌人无法抵御&#xff0c;那是攻击了敌人空虚的地方…

小程序input的placeholder不垂直居中的问题解决

input的placeholder不垂直居中&#xff0c;input设置高度后&#xff0c;使用line-height只能使输入的文字垂直居中&#xff0c;但是placeholder不会居中&#xff0c;反而会偏上。 首先placeholder样式自定义 有两种方法&#xff0c;第一种行内样式&#xff1a; <input ty…

大彩串口屏使用记录

写在最前面 屏幕型号 DC10600M070 IDE VisualTFT&#xff08;官方&#xff09; VSCode&#xff08;lua编程&#xff09; 用之前看一下官方那个1小时的视频教程就大概懂控件怎么用了&#xff0c;用官方的软件VisualTFT很简单 本文只是简单记录遇到的一些坑 lua编辑器 VisualTF…

uview ui 查看版号

版本查询2种方式 有两种方式可以查询到正在使用的uView的版本&#xff1a; // 通过console.log打印的形式 console.log(uni.$u.config.v);// 可以查阅uView的配置文件得知当前版本号&#xff0c;具体位置为&#xff1a; /uview-ui/libs/config/config.js

Linux centos7 bash编程(小练习)

一、打印九九乘法口诀 这一个for循环嵌套的小练习&#xff0c;难度不大。提供一种写法&#xff0c;供参考&#xff1a; #!/bin/bash # 文件名&#xff1a;99table.sh # 打印输出九九乘法口诀表 for i in {1..9} do for ((j1;j<$i;j)) do …

R-Meta分析核心技术教程

详情点击链接&#xff1a;全流程R-Meta分析核心技术教程 一&#xff0c;Meta分析的选题与检索 1、Meta分析的选题与文献检索 1)什么是Meta分析 2)Meta分析的选题策略 3)精确检索策略&#xff0c;如何检索全、检索准 4)文献的管理与清洗&#xff0c;如何制定文献纳入排除标准 …

【Git】测试持续集成——Git+Gitee+PyCharm

文章目录 概述一、使用Gitee1. 注册账号2. 绑定邮箱3. 新建仓库4. 查看项目地址 二、安装配置Git1. 下载安装包2. 校验是否安装成功。3. 配置Git4. Git命令5. Git实操 三、PyCharmGit1. 配置Git2. Clone项目3. 提交文件到服务器4. 从服务器拉取文件 概述 持续集成&#xff08;…

开源在物联网(IoT)中的应用

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

react解决死循环方法?

使用useeffect&#xff08;副作用&#xff09;方法结束这个操作 1、导入useeffect、useState 2、把下方代码写入&#xff1a;里面填写的是你要终止某个东西的代码 注意&#xff1a;不可不写&#xff0c;也可以写依赖或不写

基于Python+djangoAI 农作物病虫害预警系统智能识别系统设计与实现(源码&教程)

1.背景 随着科技的发展&#xff0c;机器学习技术在各个领域中的应用越来越广泛。在农业领域&#xff0c;机器学习技术的应用有助于提高农作物的产量和质量&#xff0c;降低农业生产的成本。本文针对农作物健康识别问题&#xff0c;提出一种基于机器学习方法的农作健康识别系统&…

Spring Boot整合RabbitMQ之路由模式(Direct)

RabbitMQ中的路由模式&#xff08;Direct模式&#xff09;应该是在实际工作中运用的比较多的一种模式了&#xff0c;这个模式和发布与订阅模式的区别在于路由模式需要有一个routingKey&#xff0c;在配置上&#xff0c;交换机类型需要注入DirectExchange类型的交换机bean对象。…

JMeter性能测试(上)

一、基础简介 界面 打开方式 双击 jmeter.bat双击 ApacheJMeter.jsr命令行输入 java -jar ApacheJMeter.jar 目录 BIN 目录&#xff1a;存放可执行文件和配置文件 docs目录&#xff1a;api文档&#xff0c;用于开发扩展组件 printable-docs目录&#xff1a;用户帮助手册 li…

docker harbor私有库

目录 一.Harbor介绍 二.Harbor的特性 三.Harbor的构成 四.Harbor构建Docker私有仓库 4.2在Server主机上部署Harbor服务&#xff08;192.168.158.25&#xff09; 4.2.1 这时候这边就可以去查看192.168.158.25网页 4.3此时可真机访问serverIP 4.4通过127.0.0.1来登陆和推送镜…

vue 中 axios 的安装及使用

vue 中 axios 的安装及使用 1. axios 安装2. axios使用 1. axios 安装 首先&#xff0c;打开当前的项目终端&#xff0c;输入 npm install axios --save-dev验证是否安装成功&#xff0c;检查项目根目录下的 package.json,其中的 devDependencies 里面会多出一个axios及其版本…

Linux(基础篇二)

Linux基础篇 Linux基础篇二5. 系统管理5.1 Linux中的进程和服务5.3 systemctl5.4 运行级别CentOS 6CentOS 7 5.5 关机重启命令 Linux基础篇二 5. 系统管理 5.1 Linux中的进程和服务 计算机中&#xff0c;一个正在执行的程序或命令&#xff0c;被叫做“进程”(process) 启动之…

OpenCV实战(基础知识三)

简介 OpenCV是一个流行的开源计算机视觉库&#xff0c;由英特尔公司发起发展。它提供了超过2500个优化算法和许多工具包&#xff0c;可用于灰度、彩色、深度、基于特征和运动跟踪等的图像处理和计算机视觉应用。OpenCV主要使用C语言编写&#xff0c;同时也支持Python、Java、C…

网络安全(红队)自学学习路线

想自学网络安全&#xff08;黑客技术&#xff09;首先你得了解什么是网络安全&#xff01;什么是黑客&#xff01; 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全…

UE4 地形编辑基础知识 学习笔记

之前自己写过这样的功能&#xff0c;今天看到一个UE现成的 点击地形&#xff0c;选择样条 按住CTRL键点击屏幕中某一个点会在场景内生成一个这样的图标 再点两次&#xff0c;会生成B样条的绿线条 点击号再选择一个模型&#xff0c;会生成对应的链条状的mesh 拉高最远处的一个图…

python网络爬虫指南二:多线程网络爬虫、动态内容爬取(待续)

文章目录 一、多线程网络爬虫1.1 线程的基础内容、GIL1.2 创建线程的两种方式1.3 threading.Thread类1.4 线程常用方法和锁机制1.5 生产者-消费者模式1.5.1 生产者-消费者模式简介1.5.2 Condition 类协调线程 1.6 线程中的安全队列1.6 多线程爬取王者荣耀壁纸1.6.1 网页分析1.6…