旅游网站经营模式,在屈臣氏做网站运营,泰州哪家做网站建设比较好,中国建筑app下载官网【Linux网络编程三】Udp套接字编程网络应用场景 应用场景一#xff1a;远程命令执行应用场景二#xff1a;与Windos端相互通信应用场景三#xff1a;简单聊天1.多线程化2.输入输出分开 应用场景一#xff1a;远程命令执行
简单的服务器上一篇已经完成#xff0c;接下来我… 【Linux网络编程三】Udp套接字编程网络应用场景 应用场景一远程命令执行应用场景二与Windos端相互通信应用场景三简单聊天1.多线程化2.输入输出分开 应用场景一远程命令执行
简单的服务器上一篇已经完成接下来我们就可以加一些应用场景了。我们知道客户端发送消息给服务器服务器对消息加工处理再发送回去。但谁规定客户端只能发送消息的呢我们可以发送一些命令让服务帮我执行然后将执行结果再发送回来。
服务器的对数据的加工处理动作是可以分离出来的实现代码层面的分层。 通过函数回调的方法由上层来决定加工处理动作。
所以我们只需要在服务器内部定义一个函数指针将客户端发送的数据传递给函数。调用该函数由上层传入函数来实现回调。
所以我们想实现对客户端佛发送的命令进行执行然后将执行结果再发送回去。 我们利用popen()这个系统调用接口。它可以将传递进行的以字符串形式的命令通过程序替换的形式执行。并且是自动创建子进程让子进程程序替换并且创建父子管道将执行的结果通过管道返回回来。 返回的结果存放在一个文件里我们可以通过文件描述符来访问。 #include Udpserver.hpp
#include memory
#include cstdio
#include stdlib.hstd::string ExcuteCommand(const std::string cmd)
{std::coutget a request cmd: cmdstd::endl;//服务器端接收到命令后会先打印获取到一个命令//获取到命令后就让服务器进行运行处理FILE* fppopen(cmd.c_str(),r);//popen会自动创建子进程让子进程进行程序替换运行字符串命令然后会建立父子间的管道将运行结果发送给父进程if(fpnullptr){perror(popen);return error;}std::string reslut;char buffer[1024];while(true){char*rfgets(buffer,sizeof(buffer),fp);//按行读取读取一行if(rnullptr)break;reslutr;}pclose(fp);return reslut;
}
*/
#include Udpserver.hpp
#include memory
#include cstdiovoid Usage(std::string proc)
{std::cout\n\rUsage: proc port[1024]\nstd::endl;
}
//服务器进程启动时按照./Udpserverport的形式传递
int main(int args,char* argv[])
{if(args!2){Usage(argv[0]);exit(0);}uint16_t portstd::stoi(argv[1]);std::unique_ptrUdpserver svr(new Udpserver(port));//首先创建一个服务器对象指针//智能指针用一个UdpServer指针来管理类对象svr-Init();//初始化服务器svr-Run(ExcuteCommand);//启动服务器return 0;
} 通过这个场景我们就应该能够理解Xshell的本质就是一个客户端而启动时连接ip登入密码就是在连接服务器。然后我们输入的命令将发送给xhell服务器然后服务器将执行结果再发送给我们。
应用场景二与Windos端相互通信
我们在Linux下写的网络通信其实在window下也是可以进行通信的虽然两个操作系统不同但是网络协议栈是相同的所以它们是具有相同的接口的。 只不过在Windows下还需要几个windows库要使用其他的都跟linux下是一样的。所以我们是可以在Linux下和Windows下互相通信的让linux下程序充当服务器让windows下的程序充当客户端。 以上就是在windows下通信需要的准备工作需要包含一个新的头文件要注意必须先包含该头文件在包含下面的lib32库。 然后对库进行初始化。最后不使用了就关闭库。中间就是我们的网络通信部分。 这里还有一个细节就是在windows下将字符转换成int字节的函数inet_pton()会显示没有定义吗但是是定义成功的所以我们选择忽略这个警告 【Windows下的客户端】 #include iostream
#include WinSock2.h
#include Windows.h
#include cstdlib
#include cstdio
#include string
#pragma comment(lib,ws2_32.lib)#pragma warning(disable:4996)
std::string serverip 112.124.70.128;
//一般app客户端内置服务器的ip地址这里我们就直接连接我们的linux服务器ip地址
uint16_t serverport 3555;
int main()
{WSADATA wsd;WSAStartup(MAKEWORD(2, 2), wsd);struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_addr.s_addr inet_addr(serverip.c_str()); // 将string类型转换成int类型并且是网络字节序//在windos下回存在警告忽略即可server.sin_port htons(serverport);int len sizeof(server);// 1.创建套接字---本质就是打开网络文件int sockfd socket(AF_INET, SOCK_DGRAM, 0);if (sockfd SOCKET_ERROR ){std::cout socket create err std::endl;return 1;}// 创建成功// 2.需要绑定吗系统会给我自动绑定首次发送的时候就会绑定// 3.往服务器的套接字里发送消息--需要知道服务器的ip和端口号,目的ip和目的port,将ip和port填入结构体对象里char outbuffer[1024];std::string message;while (true){std::cout Please enter ;std::getline(std::cin, message);//1.发送给服务器sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr*)server, len);struct sockaddr_in temp;int l sizeof(temp);//2.接收服务器的应答int s recvfrom(sockfd, outbuffer, 1023, 0, (struct sockaddr*)temp, l);if (s 0){//接收成功outbuffer[s] 0;std::cout outbuffer std::endl;}}//close(sockfd);closesocket(sockfd);WSACleanup();return 0;
}Linux和windows下的编码不同所以在windows下接收linux下加工的消息就会存在不同。 【Linux下的服务器】 -0,0 1,103
#pragma once#include Log.hpp
#include iostream
#include string
#include strings.h
#include cstring
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include functional
std::string defaultip 0.0.0.0;
uint16_t defaultport 8080;
Log lg; // 日志默认往显示屏打印
typedef std::functionstd::string(const std::string) func_t;//相当于定义了一个函数指针
//返回值是string类型函数参数也是string类型利用函数回调的方法将服务器端对数据的处理操作进行分离由上层传递的函数来决定如何处理
enum
{SOCKET_ERR 1,BIND_ERR
};
class Udpserver
{
public:Udpserver(const uint16_t port defaultport, std::string ip defaultip) : _sockfd(0), _port(port), _ip(ip){}void Init(){// 1.创建udp套接字本质就是打开网络套接字文件_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0) // 表示创建失败{lg(Fatal, socket create error,socket: %d, _sockfd);exit(SOCKET_ERR); // 创建失败之间退出}// 创建成功lg(Info, socket create success,socket: %d, _sockfd);// 2.绑定服务器的套接信息比如ip和端口号// 在绑定套接信息之前需要先将对应的结构体对象填充完毕sock_addrstruct sockaddr_in local; // 网络通信类型bzero(local, sizeof(local)); // 将内容置为0local.sin_family AF_INET; // 网络通信类型local.sin_port htons(_port); // 网络通信中端口号需要不断发送所以需要符合网络字节序,主机---网络字节序local.sin_addr.s_addr inet_addr(_ip.c_str()); // 需要将string类型的ip转换成int类型并且还需要满足网络字节序的要求socklen_t len sizeof(local);// 以上只是将要绑定的信息填充完毕套接字(网络文件)而还没有绑定套接信息if (bind(_sockfd, (const struct sockaddr *)local, len) 0) // 绑定失败{lg(Fatal, bind sockfd error,errno%d,err string:%s, errno, strerror(errno));exit(BIND_ERR);}lg(Info, bind sockfd success,errno%d,err string:%s, errno, strerror(errno)); // 绑定成功}void Run(func_t func) // 服务器是一旦启动不会退出服务器接收消息并发送答应{// 1.接收信息char buffer[SIZE];while (true){struct sockaddr_in client;socklen_t len sizeof(client);// 服务器接收到消息它还需要知道谁给它发送的为了后续将应答返回过去// 利用一个输出型参数将对方的网络信息填充到里面ssize_t n recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)client, len);if (n 0){lg(Warning, recvfrom sockfd err,errno: %d, err string %s, errno, strerror(errno));continue;}// 读取成功将网络文件里的消息读取到buffer里buffer[n] 0; // 字符串形式// 2.加工处理// std::string info buffer;// std::string echo_string server echo# info;std::string infobuffer;std::string echo_stringfunc(info);//将接收的信息由外层函数进行处理// 3.将应答发送回去// 发送给谁呢服务器知道吗服务器知道因为在接收消息时服务器就用一个输出型参数将客户端的网络消息保存下来了sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr *)client, len);}}~Udpserver(){if (_sockfd 0)close(_sockfd);}private:int _sockfd; // 套接字文件描述符std::string _ip; // 我们习惯用string类型的ip地址uint16_t _port; // 服务器进程的端口号
};
应用场景三简单聊天
我们还可以制作一个简单的聊天软件平常我们往群里输入一个消息qq群里上就会显示我发送了一条消息。并且还标明了是谁发送的。比如李四往群里发送:“hello world”。群里的人都能看到这条消息李四“hello world”。
当客户端给服务器发送消息时服务器不仅能接收到客户端发送的内容还可以知道客户端的网络消息(客户端的ip和端口号等)也就是知道是谁发送它。所以这里我们可以利用ip端口号来表示用户。当用户往服务器发送消息时服务器就将该用户的ip地址端口号来表示该用户然后将以[ipport]:消息再发送回去。显示在屏幕上。 注意客户端发送消息给服务器服务器可以获取到客户端的ip地址和端口号等网络信息但这些信息都是网络字节序的如果服务器想使用需要先转换成用户字节序的。 1.可以使用ntohs()接口将端口号从网络字节序转用户字节序 2.可以使用inet_ntoa()将int类型的ip地址转string类型的ip地址并转换成用户字节序。 还有就是我往群里发送的消息qq群里的人是不是都能看到呀。所以我们应该让所有连接服务器的客户端用户都能看到我发送的消息。而他们往群里发送的消息我也可以看到。
所以我们需要将所有在线的客户端都能看到消息我们需要制作一个在线用户列表。 我们用一个哈希表来存储在线用户。当客户端用户发送消息时服务器端就会接收消息根据用户的ip地址来到哈希表里检测哈希表里是否有该用户如果没有就添加进去如果有则什么都不做。这样就将用户添加到哈希表里了所有发送过消息的客户端用户都会被添加进去但是同一个ip的只能添加一个客户端。
服务器接收消息并加工处理后就可以发送给所有在线的客户端用户了广播发送给所有用户那么这些用户在哪呢在哈希表里(只要有新客户端用户发送消息了就会将该客户端ip添加到哈希表里哈希表kv结果k表示ip地址v表示客户端的网络信息) #pragma once#include Log.hpp
#include iostream
#include string
#include strings.h
#include cstring
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include functional
#include unordered_map
std::string defaultip 0.0.0.0;
uint16_t defaultport 8080;
Log lg; // 日志默认往显示屏打印
typedef std::functionstd::string(const std::string info, uint16_t port, const std::string ip) func_t; // 相当于定义了一个函数指针
// 返回值是string类型函数参数也是string类型利用函数回调的方法将服务器端对数据的处理操作进行分离由上层传递的函数来决定如何处理
enum
{SOCKET_ERR 1,BIND_ERR
};
class Udpserver
{
public:Udpserver(const uint16_t port defaultport, std::string ip defaultip) : _sockfd(0), _port(port), _ip(ip){}void Init(){// 1.创建udp套接字本质就是打开网络套接字文件_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0) // 表示创建失败{lg(Fatal, socket create error,socket: %d, _sockfd);exit(SOCKET_ERR); // 创建失败之间退出}// 创建成功lg(Info, socket create success,socket: %d, _sockfd);// 2.绑定服务器的套接信息比如ip和端口号// 在绑定套接信息之前需要先将对应的结构体对象填充完毕sock_addrstruct sockaddr_in local; // 网络通信类型bzero(local, sizeof(local)); // 将内容置为0local.sin_family AF_INET; // 网络通信类型local.sin_port htons(_port); // 网络通信中端口号需要不断发送所以需要符合网络字节序,主机---网络字节序local.sin_addr.s_addr inet_addr(_ip.c_str()); // 需要将string类型的ip转换成int类型并且还需要满足网络字节序的要求socklen_t len sizeof(local);// 以上只是将要绑定的信息填充完毕套接字(网络文件)而还没有绑定套接信息if (bind(_sockfd, (const struct sockaddr *)local, len) 0) // 绑定失败{lg(Fatal, bind sockfd error,errno%d,err string:%s, errno, strerror(errno));exit(BIND_ERR);}lg(Info, bind sockfd success,errno%d,err string:%s, errno, strerror(errno)); // 绑定成功}void Checkuser(struct sockaddr_in client,uint16_t clientport,const std::string clientip){//用该ip检测哈希表里是否存在该用户auto iteronline_user.find(clientip);if(iteronline_user.end())//说明没有找到 是新用户{online_user.insert({clientip,client});std::coutadd a user:[clientip:clientport]std::endl; }//如果不是新用户那么什么都不干}void Broadcast(const std::string info,uint16_t clientport,const std::string clientip){//将消息加工处理广播发送给所有在线的用户而这些用户都在哈希表里for(auto user:online_user){std::string message[处理客户端数据 ;messageclientip;message:;messagestd::to_string(clientport);message];messageinfo;sendto(_sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)(user.second), sizeof(user.second));}}void Run(/*func_t func*/) // 服务器是一旦启动不会退出服务器接收消息并发送答应{// 1.接收信息char buffer[SIZE];while (true){struct sockaddr_in client;socklen_t len sizeof(client);// 服务器接收到消息它还需要知道谁给它发送的为了后续将应答返回过去// 利用一个输出型参数将对方的网络信息填充到里面ssize_t n recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)client, len);if (n 0){lg(Warning, recvfrom sockfd err,errno: %d, err string %s, errno, strerror(errno));continue;}// 读取成功将网络文件里的消息读取到buffer里buffer[n] 0; // 字符串形式// 服务器想知道是谁给它发送消息的//①获取到发送者的消息②也获取到发送者的套接字信息所以我们怎么知道是谁发送过来的呢uint16_t clientport ntohs(client.sin_port); // 注意port是网络字节序需要转成用户字节序std::string clientip inet_ntoa(client.sin_addr); // ip是网络字节序的int类型需要转换成string类型Checkuser(client,clientport,clientip);//在线用户列表 std::string info buffer;//接收消息后就广播发送给所有在线的人,并告诉是谁发送的Broadcast(info,clientport,clientip);// std::string info buffer;// std::string echo_string server echo# info;// std::string infobuffer;// std::string echo_stringfunc(info,clientport,clientip);// 将接收的信息由外层函数进行处理// 3.将应答发送回去// 发送给谁呢服务器知道吗服务器知道因为在接收消息时服务器就用一个输出型参数将客户端的网络消息保存下来了//sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr *)client, len);}}~Udpserver(){if (_sockfd 0)close(_sockfd);}private:int _sockfd; // 套接字文件描述符std::string _ip; // 我们习惯用string类型的ip地址uint16_t _port; // 服务器进程的端口号std::unordered_mapstd::string, struct sockaddr_in online_user; // 网上在线列表// 服务器收到一个客户信息就要检测这个客户是不是新用户如果是就添加进去
};
1.多线程化
不过客户端是存在问题的什么问题呢用户是不是也能看到别人发送的消息 是的但是目前的客户端要求用户必须先输入才能获取服务器发送的消息也就是我不输入也就看不到别人的消息。只有我输入了。而正常聊天应该是我不输入我也能看到对方发送的消息应该。 这就要求输入和输出两个动作要同时运行我们可以使用多线程让两个线程各自执行一个线程执行发送一个线程执行读取这样就算我输入也能接收到别人发送的消息。 【理论依据】 发送线程需要往套接字里发送并且需要知道发送给谁所以需要构建一个结构体对象封装套接字和服务器网络信息。 接收线程需要从套接字里读取。通过结构体对象里的套接字。 所以我们可以利用该结构来作为线程函数的参数。让线程拿到。在之前先将内容初始填充。 #include iostream
#include cstdlib
#include unistd.h
#include strings.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.husing namespace std;#include iostream
#include strings.h
#include sys/types.h
#include cstring
#include unistd.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include pthread.husing namespace std;
// 客户端
void Usage(std::string proc)
{std::cout \n\r./Usage: proc serverip serverport\n endl;
}struct PthreadData
{int sockfd;struct sockaddr_in server;
};
void *Sender(void *args)
{while (true){PthreadData *pd static_castPthreadData *(args);socklen_t len sizeof(pd-server);string message;cout Please enter ;getline(cin, message);// 1.发送给服务器sendto(pd-sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)(pd-server), len);}
}
void *Revcer(void *args)
{while (true){PthreadData *pd static_castPthreadData *(args);char outbuffer[1024];struct sockaddr_in temp;socklen_t l sizeof(temp);// 2.接收服务器的应答ssize_t s recvfrom(pd-sockfd, outbuffer, 1023, 0, (struct sockaddr *)temp, l);// 共用一个套接字但不影响是线程安全的if (s 0){// 接收成功outbuffer[s] 0;cerr outbuffer endl;}}
}
// 启动客户端时要求是 ./Client 服务器ip 服务器port
int main(int argc, char *argv[])
{if (argc ! 3){Usage(argv[0]);exit(0);}std::string serverip argv[1];uint16_t serverport std::stoi(argv[2]);PthreadData pd;bzero((pd.server), sizeof(pd.server));pd.server.sin_family AF_INET;pd.server.sin_addr.s_addr inet_addr(serverip.c_str()); // 将string类型转换成int类型并且是网络字节序pd.server.sin_port htons(serverport);// 1.创建套接字---本质就是打开网络文件pd.sockfd socket(AF_INET, SOCK_DGRAM, 0);if (pd.sockfd 0){cout socket create err endl;return 1;}// 创建成功// 2.需要绑定吗系统会给我自动绑定首次发送的时候就会绑定// 3.往服务器的套接字里发送消息--需要知道服务器的ip和端口号,目的ip和目的port,将ip和port填入结构体对象里pthread_t sender, revcer;pthread_create(sender, nullptr, Sender, pd);pthread_create(revcer, nullptr, Revcer, pd);pthread_join(sender, nullptr);pthread_join(revcer, nullptr);close(pd.sockfd);return 0;
}
这样一个线程一直在读取只要有人往套接字里发送消息了服务器就能直接返回回来我就可以获取到不需要输入就可以获取到。因为另一个线程在输入。两个线程是并发的。
2.输入输出分开
上面还存在一个问题输入和输出混乱在同一个显示屏里混乱输出。体验感不好。 所以我们想让输入和输出分开显示。怎么做呢通过终端通过打开多个终端让输入输出显示在不同的终端上。
首先就是让服务器端加工处理后的数据利用错误输出流输出。而客户端发送消息默认输出在当前显示屏上。 然后再二号错误输出流重定向到一个新打开的终端显示器上。这样最后服务器发送回来的消息就会显示在新打开的终端上。而客户端输出就默认输入在旧的终端。