实现目标
- 实现一个服务端和一个客户端,客户端负责发送一个单词,服务端接收到后将翻译后的结果返回发送到客户端。
- 使用UDP网络连接,可以跨主机实现通信。
- 服务端读取文件中保存的单词及其翻译,通过发送信号使服务端更新词库,不需要重启。
认识相关接口
socket
创建套接字文件,在Linux一切皆文件。。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol);
参数一为需要选择的通信方式:
通常是使用AF_UNIXAF_INET,分别表示为本地通信和网络通信。
参数二为套接字提供服务的类型,通常使用SOCK_STREAM:流式服务TCP策略,SOCK_DGRAM:数据报服务,UDP策略
参数三默认设为0即可,因为前面两个参数已经确定好通信的方式和策略
返回值:成功创建返回文件的文件描述符, 失败返回-1
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); assert(_sockfd != -1); cout << "success : " << _sockfd << endl;
bzero
可以将结构体对象初始化,和memset同理
#include <strings.h> void bzero(void *s, size_t n);
bind
绑定端口号
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数一为:需要绑定的文件描述符
参数二为:sockaddr结构体对象的地址,通常使用sockaddr_in对象强转,这个结构体对象里面就包括了传输方式,端口号,和ip地址
参数三为:这个结构体对象的大小
成功返回0
assert(bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) == 0);
recvfrom
读取数据。
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
参数一为:文件描述符
参数二为:接收数据的存储对象
参数三为:接收数据的存储对象的大小
参数四默认为0,表示阻塞读取
参数五为:一个结构体对象,输入输出型参数,该对象接收到后里面包含了发送端的信息,以便在未来可以往这个位置发回信息。
参数六为:接收到这个结构体对象的大小
成功返回数据的字节数,失败返回-1
ssize_t s = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
sendto
发送数据
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
参数一为:文件描述符
参数二为:发送的数据的缓冲区
参数三为:发送数据的长度
参数四默认为0,阻塞发送
参数五为:结构体对象,里面包含了接收端的属性,ip地址等
参数六为:结构体对象大小
sendto(sockfd, res.c_str(), res.size(), 0, (sockaddr *)&client, sizeof(client));
实现思路和注意事项
思路:
- 首先可以对客户端和服务端分别进行封装
- 两者都具有初始化,启动功能。初始化主要负责初始化自身的IP地址,端口号和通信方式等
- 两者的启动都必须要有发送和读取的功能,客户端先发送再读取,服务端先读取再发送
- 服务端要有一个接收到数据后的回调函数,对数据进行处理后再发送回去
- 使用C++文件操作,加载文件里的词库
注意事项:
- 运行服务端时必须带上端口号,运行客户端必须带上IP地址和端口号
- 服务端必须显示绑定端口号,客户端不需要。操作系统会帮客户端自动生产并绑定端口号,因为服务端是只有一个,而访问这个服务端的客户端却会有很多个。
- 服务端的IP地址不能够指定某个特定的IP地址,必须使用0.0.0.0,因为会有很多个客户端访问,如果指明一个特定的IP地址,那么就可能出现别的IP访问不了端口号
- 注意端口号必须要调用接口去转换一下大小端,因为很多情况下都不清楚机器的大小端,养成好习惯
- 所有接口的参数都是 sockaddr类型的结构体,但是在使用的时候往往都是定义 sockaddr_in 结构体,传参的时候再强转。sockaddr_in的属性分别为:sin_family 传输方式;sin_port 端口号;sin_addr.s_addr IP地址
完整代码
以下代码均有注释,上述不完整的代码的注释里都有解释
Server.hpp
#pragma once #include <iostream> #include <string> #include <strings.h> #include <cassert> #include <unistd.h> #include <functional> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> using namespace std; typedef function<void(int, string, uint16_t, string)> func_t; class udpServer { public: udpServer(const uint16_t &port, const func_t &funcCall) : _port(port), _ip("0.0.0.0"), _funcCall(funcCall) { } // 初始化服务器端 void initServer() { // 创建socket _sockfd = socket(AF_INET, SOCK_DGRAM, 0); assert(_sockfd != -1); cout << "success : " << _sockfd << endl; // 定义socket_in结构体变量 struct sockaddr_in local; // 初始化这个变量 bzero(&local, sizeof(local)); // 填充这个变量里的属性 local.sin_family = AF_INET; // 指定传输方式 // 指定端口号,不明确大小端所以要调用一下转换函数 local.sin_port = htons(_port); // 指定IP地址, 首先要把字符串类型转换成网络IP的整型再转换大小端 // 一般而言不会指明一个特定的IP地址,而是会设为0.0.0.0 // 因为如果只绑定一个明确的IP,最终的数据可能用别的IP来访问端口号就会访问不了 // INADDR_ANY就是0.0.0.0 // local.sin_addr.s_addr = inet_addr(_ip.c_str()); local.sin_addr.s_addr = INADDR_ANY; // 绑定端口号 assert(bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) == 0); } // 启动服务器端 void start() { char buff[1024]; // 服务器本质就是一个死循环,除非紧急情况否则不退出 while (1) { struct sockaddr_in peer; // 保存这个结构体大小的变量 socklen_t len = sizeof(peer); ssize_t s = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len); if (s > 0) { // 记录数据是什么,哪个IP地址发的,发到哪个端口 // 首先peer里的IP地址是网络序列,所以要转化为整形再转成点分制的字符串 string clientip = inet_ntoa(peer.sin_addr); // 端口号也要利用函数调用转换为16位的整形 uint16_t clientport = ntohs(peer.sin_port); // 保存数据 buff[s] = 0; string message = buff; // 读取数据 cout << clientip << "[ #: " << clientport << "] : " << message << endl; // 处理数据后再发回客户端 _funcCall(_sockfd, clientip, clientport, message); } sleep(1); } } ~udpServer() { } private: uint16_t _port; // 端口号 string _ip; // ip地址 int _sockfd; // 创建socket后的网络文件描述符 func_t _funcCall; // 回调方法 };
Server.cc
#include "Server.hpp" #include <memory> #include <unordered_map> #include <fstream> #include <signal.h> #define textfile "./dict.txt" // 保存字典 unordered_map<string, string> dict; // 输出命令错误函数 void Usage(string proc) { cout << "Usage:\n\t" << proc << " local_ip local_port\n\n"; } // 读取一行中的kv值 bool getString(const string &line, string *key, string *value) { auto pos = line.find(":"); if (pos == string::npos) return false; // 分割两段字符串 分别提取 *key = line.substr(0, pos); *value = line.substr(pos + 1); return true; } // 初始化字典 void Initdict() { string key, value, line; // 打开文件读取内容插入到dict中 ifstream ifs(textfile, ios::binary); if (!ifs.is_open()) { cerr << "open file error" << endl; exit(3); } while (getline(ifs, line)) { if (getString(line, &key, &value)) dict.insert(make_pair(key, value)); } ifs.close(); cout << "dict success" << endl; } // 如果收到2号信号则重新读取文件重新加载dict void reload(int signal) { Initdict(); } // 设置接收数据后的回调函数 void CallMessage(int sockfd, string clientip, uint16_t clientport, string message) { // 对接收到的数据进行自定义处理 // 与通信解耦 // 查询接收到的单词并查找 auto it = dict.find(message); string res; if (it == dict.end()) res = "未查询到"; else res = it->second; // 将查询到的结果返回去 struct sockaddr_in client; client.sin_family = AF_INET; client.sin_addr.s_addr = inet_addr(clientip.c_str()); client.sin_port = htons(clientport); sendto(sockfd, res.c_str(), res.size(), 0, (sockaddr *)&client, sizeof(client)); } int main(int argc, char *argv[]) { // 从命令行获取命令 // 其中包括端口号 // 如果分割不为两部分就说明命令有误,输出错误信息后退出 if (argc != 2) { Usage(argv[0]); exit(2); } // 拿到端口号 uint16_t port = atoi(argv[1]); // 如果收到2号信号则重新读取文件重新加载dict signal(2, reload); // 初始化字典 Initdict(); unique_ptr<udpServer> us(new udpServer(port, CallMessage)); us->initServer(); us->start(); return 0; }
Client.hpp
#pragma once #include <iostream> #include <string> #include <cstring> #include <strings.h> #include <cassert> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> using namespace std; class udpClient { public: udpClient(const string &server_ip, const uint16_t &server_port) : _server_ip(server_ip), _server_port(server_port) { } void clientInit() { _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd == -1) exit(2); cout << "success : " << _sockfd << endl; // 客户端也需要绑定IP地址和端口,但是不需要显示绑定,操作系统会自动绑定 // 客户端的端口号对服务端而言并不重要,它只需要确定自己的唯一性即可 // 相当于写服务器的是一家公司,写客户端的是无数家公司,无数家公司之间只需要不冲突即可 } void run() { string buff; struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(_server_ip.c_str()); server_addr.sin_port = htons(_server_port); while (1) { cout << "Please cin:"; cin >> buff; // sendto自动帮客户端绑定端口 ssize_t s = sendto(_sockfd, buff.c_str(), buff.size(), 0, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 接收服务端发回来的数据 char message[1024]; struct sockaddr_in temp; bzero(&temp, sizeof(temp)); socklen_t len = sizeof(temp); size_t n = recvfrom(_sockfd, message, sizeof(message) - 1, 0, (struct sockaddr *)&temp, &len); if (n > 0) message[n] = 0; cout << "翻译结果:" << message << endl; } } ~udpClient() { } private: int _sockfd; string _server_ip; uint16_t _server_port; };
Client.cc
#include "Client.hpp" #include <memory> // 输出命令错误函数 void Usage(string proc) { cout << "Usage:\n\t" << proc << " server_ip server_port\n\n"; } int main(int argc, char* argv[]) { // 从命令行获取命令 // 其中包括服务端的IP地址和对应的端口号 // 如果分割不为两部分就说明命令有误,输出错误信息后退出 if(argc != 3) { Usage(argv[0]); exit(2); } // 保存服务端的IP地址和端口号 string server_ip = argv[1]; uint16_t server_port = atoi(argv[2]); unique_ptr<udpClient> cs(new udpClient(server_ip, server_port)); cs->clientInit(); cs->run(); return 0; }
运行效果
初始词库:
运行效果:
更新后词库:
运行:不需要重启服务端,发送2号信号(ctrl + c)
END
以上就是本篇简易的UDP英汉词典了,期待各位佬们能够指点一二。