一、本篇重点
对于上一篇实现的简单udp客户端/服务器进一步的补充改造,继续了解与Socket api的相关接口
二、upd服务器(第一版)
上一篇我们通过一边实现一个简单udp,一边学习Socket编程的相关接口,本篇打算进一步对上一篇的代码进行补充和添加功能,上一篇为了了解接口,只是简单的让client端能够发送消息到server端,然后server端获取消息并能够发送回来即可,这里先提供完整的代码。
1. udp_server.hpp
#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include "errno.hpp"
#include <cstring>namespace chk
{const static uint16_t default_port = 8080;class UdpServer{public:// 对成员变量完成初始化UdpServer(uint16_t port = default_port) : _port(port){std::cout << "server port: " << _port << std::endl;}void Init() // 创建出套接字,并绑定端口号和ip{// 1. 创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);if (_sock < 0){std::cerr << "create socket error: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}std::cout << "bind socket success: " << _sock << std::endl; // 3// 2. 构建struct sockaddr_in结构体struct sockaddr_in local;bzero(&local, sizeof(local)); // 初始化local.sin_family = AF_INET; // IPv4local.sin_port = htons(_port); // 端口号local.sin_addr.s_addr = INADDR_ANY; // 服务器下的ip// 3. 绑定套接字和sockaddr_inint n = bind(_sock, (struct sockaddr *)&local, sizeof(local));if (n < 0){std::cerr << "bind error: " << strerror(errno) << std::endl;exit(BIND_ERR);}std::cout << "bind socket success: " << _sock << std::endl;}void Start() // 时刻读取套接字中的信息数据,并将其返回{char buffer[1024];while(true){// 收数据struct sockaddr_in peer;socklen_t len = sizeof(peer);int n = recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);if(n>0) buffer[n] = '\0';else continue;//收到信息后打印出来:对方ip+端口号+内容std::cout << inet_ntoa(peer.sin_addr) << " - " << ntohs(peer.sin_port) << " : " << buffer << std::endl;// 发回去sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,sizeof(peer));}}~UdpServer() // 析构{}private:int _sock;uint16_t _port;};
}
2. udp_server.cc
#include"udp_server.hpp"
#include<string>
#include<memory>
#include<cstdio>using namespace std;
using namespace chk;// 我们最终希望以 ./udp_server port 的形式去启动
static void usage(string proc)//使用手册
{std::cout << "Usage:\n\t" << proc << " port\n" << std::endl;
}int main(int argc,char* argv[])
{if(argc != 2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);unique_ptr<UdpServer> usvr(new UdpServer(port));usvr->Init();usvr->Start();return 0;
}
3. udp_client.hpp
#pragma once#include<iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
#include<string>
#include"errno.hpp"
4. udp_client.cc
#include"udp_client.hpp"//基本要用到的头文件
using namespace std;static void usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}// ./udp_client server_ip server_port
int main(int argc,char* argv[])
{if(argc != 3){usage(argv[0]);exit(USAGE_ERR);}string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);//1. 创建套接字int sock = socket(AF_INET,SOCK_DGRAM,0);if(sock < 0){cerr << "client : create socket error" << endl;exit(SOCKET_ERR);}//2. 创建server端的struct sockaddrstruct sockaddr_in server;memset(&server,0,sizeof(server));//初始化方案2server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(server_ip.c_str());server.sin_port = htons(server_port);//3. 客户端测试while(true){// 用户发送消息string messages;cout << "client : " ;cin >> messages;// 发送到socksendto(sock,messages.c_str(),messages.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 << buffer << endl;}}return 0;
}
5. errno.hpp
#pragma onceenum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};
6. makefile
.PHONY:all
all: udp_client udp_serverudp_client:udp_client.ccg++ -o $@ $^ -std=c++11
udp_server:udp_server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f udp_client udp_server
三、udp服务器(第二版)
1. 添加回调函数
第一版的udp服务器,我们只要求能够收发信息,但我们实际应用中,服务端接受到信息以后,还需要对信息进行后续的处理,而具体的处理方法,我们这里简单设计几个例子。
所以我们应该对udp_server.hpp的设计进行补充和改进,我们对类的设计可以多添加一个表示具体执行方法的成员,由外部传入具体的方法,类内只需要拿到方法后,以回调的方式去处理接受到的信息,并且将处理好的信息发送回给客户端。
———————————————————————————————————————————
知识点一
在c语言中,通常是用typedef的方式去定义一个函数指针类型,而在C++中,例如我想定义一个类型string fun_t(string msg) 这样一个类型的函数指针,我们通常使用以下方式:
#include <functional>using func_t = std::function<std::string(std::string)>;
ps:这里用到的方法细究的话,知识点有点多,可以先简单的认为,这就是定义一个函数指针的方法,实际用处更加广泛,这个本质也不是函数指针。
———————————————————————————————————————————
基于上述在知识点,我们定义函数指针,添加函数指针变量的类成员对象,然后调整下构造,选择让外部传入具体方法,这样就实现了方法处理和网络传输的解耦,然后再对于之前的逻辑进行调整,我们拿到数据后,先将数据交给回调函数,然后将处理好后的信息再发生给客户端
2. 信息处理函数
这里我们简单的写几个功能进行测试即可
2.1 大小写转换
先简单实现一个,将对方发送过来的字符串信息中,关于小写的字母转换成大写返回
———————————————————————————————————————————
知识点一
这里是整理了一下要实现这个函数的一些接口,不算新的知识点
字符检查相关接口
#include<ctype.h>int isalpha(int c); // 检查是否为字母
int islower(int c); // 检查是否为小写字母
int isupper(int c); // 检查是否为大写字母
英文字符大小写转化的相关接口
#include <ctype.h>int toupper(int c); //小写转化成大写
int tolower(int c); //大写转化成小写
———————————————————————————————————————————
代码参考
string transacationString(string msg)
{string res;char tmp;for(auto& c:msg){if(islower(c)){tmp = toupper(c);res.push_back(tmp);}else{res.push_back(c);}}return res;
}
2.2 远程指令控制
这里要做一个让远程的客户端传指令给服务器,服务器处理并将结果返回,我们可以像之前一样,去封装一个命令行解释器,调用创建线程去处理并将结果返回,这里不选择自己造轮子,而是通过已有的接口去直接实现这个命令的处理,并返回相对应的处理结果。
———————————————————————————————————————————
知识点一:命令行执行接口
FILE *popen(const char *command, const char *type);
作用:创建一个子进程去执行command命令,并且创建管道连接这个子进程,通过type指定去连接到这个指令的标准输出或者标准输入
command
:要执行的 shell 命令字符串。type
:指定管道的方向,可以是"r"
(读)或"w"
(写)。如果是"r"
,则创建的管道连接到命令的标准输出,可以从这个管道读取命令的输出。如果是"w"
,则连接到命令的标准输入,可以向这个管道写入数据作为命令的输入。- 如果成功,返回一个指向
FILE
类型的指针,可以使用标准 I/O 函数(如fread
、fwrite
、fgets
等)来与管道进行交互。- 如果失败,返回
NULL
,并设置errno
来指示错误原因。
int pclose(FILE *stream);
作用:pclose
函数用于关闭由popen
函数创建的管道,并等待与该管道关联的进程结束。
stream
:由popen
函数返回的指向管道的FILE
指针。
———————————————————————————————————————————
参考代码
// 远程指令
std::string excuteCommand(std::string command)
{//1. 安全检查:避免对方输入一些你不愿意提供的指令,例如rm等等//这部分我们只是简单做测试,就不进行安全检查了//2. 业务处理逻辑FILE* fp = popen(command.c_str(),"r");if(fp == nullptr) return "Invalid instruction";//3. 获取结果char line[1024];std::string res;while(fgets(line,sizeof(line),fp)){res += line;}pclose(fp);return res;
}
四、udp服务器(第三版)
服务端除了这种对信息的处理然后返回,我们还可以简单的做一个群聊功能玩玩。
群聊的要求就是,我们多个客户端向服务器发送消息,服务器首先需要每个都拿到,并且将受到的消息,广播给每一个在线用户,要实现这些,首先要解决的就是,我需要将消息发送到每一个客户端上,我就要有一个存放每一个在线用户信息的一个容器,这里做的稍微简单一点,我们认为只要给我发送了消息,我就将你的客户端信息记录起来,认为你当前处于“在线状态”,然后将你的信息向每一个用户进行发送,此时如果有其他用户也给服务端发送信息怎么办?这就涉及到多线程并发访问的问题,我们利用前面学习到的生产消费模型去处理,多个线程发送消息到循环队列中,而服务器每次往循环队列拿信息,并且广播。而为了测试时观赏性更强一点,我们还可以利用管道文件,将内容输入重定向到管道文件中,再另外开一个窗口,把管道文件的内容打印出来。
参考代码
Linux —— udp实现群聊代码-CSDN博客
总结
本篇重点是延续上一篇,进一步改造了udp服务端,简单的补充和添加了几个功能,并且提供了代码进行参考。