ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

【Linux网络编程】--UDP__SCOKE

【Linux网络编程】--UDP__SCOKE 下面我们先对服务器端进行封装。一、简单回显服务器我们要实现一个简单回显服务器和客服端的数据回显客户端进行发送消息服务器收到消息后再将这个消息回显给我们的用户端。就类似我们在Linux上使用的echo指令。整个程序的核心就是套接字的相关接口还有使用UDP协议进行实现。首先我们需要两个部分一个是服务器端用户端。如上所示。下面我们先对服务器端进行封装。EchoServer.hpp1、初始化Init首先对于一个服务器我们初始时要对其进行初始化操作然后再去启动那么我们需要一个Init接口然后初始化完成我们再进行启动所以还需要一个启动接口Start前面我们学习系统部分的时候讲到Linux下一切皆文件不论我们是使用UDP还是使用TCP那么我们进行网络通信前都要将网络文件先打开那么我们要打开一个网络文件要使用下面这个接口要使用其要包含头文件sys/socket.h下面我们来看其返回值其返回值是一个整数那么其返回值打开成功那么就返回的是一个文件描述符fd。然后失败返回-1并且会设置其错误信息。下面是对其参数的认识首先是domain其是一个域其实就是我们未来创建这个网络文件这个文件的使用范围就比如我们是要进行本地通信还是跨网络通信如上所示如果我们想要的是本地通信那么就传入AF_UNIX如果是网络通信那么就传入AF_INET。其他的我们不需要太过关心。然后是type这个参数就是确定我们通信的类型就比如说是我们的字节流还是用户数据报的方式。第三个参数就是我们要使用的那种协议就比如是使用UDP协议还是TCP协议但是前面第二个参数设置好了那么通信的协议基本就确定了所以一般这个参数我们设置为0.所以对于socket的参数我们设置的也基本是固定的第一个参数我们设置为AF_INET第二个参数就在S0CK_STREAM和SCOK_DGRAM上选择第三个参数就设置为0即可。所以就只有第二个参数是变化的那么其第一个就是我们的TCP第二个就是UDP。所以未来我们的服务器端需要一个文件描述符表示我们打开的网络文件的fd。如上我们就可以将一个网络文件套接字创建了那么对于错误信息的输出我们可以引入前面我们写的日志系统进行打印。那么我们上面将创建好的网络文件的文件描述符进行打印那么其文件描述符肯定为3那么我们初始化首先要创建一个套接字socke他那么其本质上就是创建一个网络文件。那么我们的网络有其IP信息还有其端口号那么我们也要将其进行设置那么我们的成员变量还要设置一个ip和端口号但是有个问题就是我们前面所看到的端口号是192.1.1.0这种样式的这个叫做字符串类型的IP但是我们发现其是由4个整数和三个点组成的这种IP地址为字符串风格的点分十进制IP地址是让人看的。那么其大小就要达到32字节了所以在网络端其是会转换成4字节IP的。如上就是我们的IP和端口号对于服务器来说其_port必须是固定的。这个是由用户进行设置的。未来在使用上我们希望其和我们在Linux下使用一样./server_udp ip port这个样式进行使用那么在我们的主程序中我们可以如下进行设置这个是描述网络socket的结构体可以看到其中包括了sin_port这个就是我们的端口号然后还有的就是sin_addr这个就是我们的id。后面是sin_zero是填充字段我们不需要关注。然后其还包含了AF_INET但是有个疑问了就是我们创建socket的时候就已经传入了AF_INET了那么为啥我们这边也要有呢这是给我们后续使用别的函数接口的时候进行判断使用的。但是我们发现上面的结构体并没有出现AF_INET其实是在第一个那个宏定义中其就被替换成下面的东西然后上面的##在宏中其会将两边的进行拼接那么我们传入的sin_那么就会被替换成sin_family那么总的就是sa-family_t sin_famliy。这个数据类型实际上就是一个无符号长整型。所以我们对于网络信息的填充如下然后有个问题就是这个数据是否已经存在我们打开的网络文件呢实际上并没有所以下面我们还需要将这个信息和我们打开的socket进行绑定。首先发送方的IP和port都要带入到网络中 所以我们在服务器端要将IP和port进行处理。如下下面进行socket和网络信息进行绑定。要使用下面这个接口首先第一个参数就是我们要进行绑定的socket返回值一样成功返回0失败返回-1并且设置错误信息。第二个参数我们发现是一个struct sockaddr类型的结构体那么我们需要将我们的信息进行强制类型转换第三个参数就是我们的长度。如下所以总的初始化代码如下void Init() { // 第一步创建套接字socket本质就是打开一个网络文件 _socketfd socket(AF_INET, SOCK_DGRAM, 0); if (_socketfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_ERR); } LOG(LogLevel::INFO) create scoket success,socket: _socketfd; // 第二步设置网络信息IP, struct sockaddr_in local; bzero(local, sizeof(local)); local.sin_family AF_INET; local.sin_addr.s_addr inet_addr(_ip.c_str()); local.sin_port htons(_port); // 第三步,将信息设置到socket中 int n bind(_socketfd, (struct sockaddr *)local, sizeof(local)); if(n0) { LOG(LogLevel::FATAL)create socket error; exit(BIND_ERR); } LOG(LogLevel::INFO)bind socket success,ip:_ipport:_port; }2、运行Start在服务器运行中其实际上是一个死循环但是对于数据的发送和接收其不再是前面的字节流文件流等所以我们使用下面的函数首先是其返回值就是我们所读到多少字节的数据。失败就返回-1。下面我们来看其参数第一个参数scokfd就是我们要绑定的那个网络文件。第二个参数其是一个缓冲区未来就是用来存放我们收到的数据的实际上是一个void*类型 的那么其未来可以接收不同类型的数据。第三个参数size_t len就设置为我们缓冲区的长度即可。第四个参数就是我们要其阻塞读取还是非阻塞读取我们就设置为0阻塞读取即可。第五个参数和上面使用bind的类似。用来获取对方ip和port的数据第六个参数和bind的第三个参数一样补充未来我们获取对方的发送的信息的时候那么我们不仅需要信息本身还需要获取到发送方是谁方便后续回复所以需要第五个参数获取发送方的IP和prot。代码如下对于收信息错误我们并不需要退出。上面我们对于数据我们先认为传递的是字符串。那么我们服务器端接收到消息那么就要将这个消息发送给接收方用户。那么就要使用到下面这个接口首先其返回值其失败的话也是返回-1成功那么就会返回发送的字节数。对于发送数据的问题有下面三个疑问首先是我们要发送什么数据然后我们要发送给谁如何发送给对方。那么在这个发送函数中也要我们传入一个缓冲区就是第二个参数。那么发送给谁那么就是发送给对方的IPprot。那么就是最后两个参数。发送的方式是通过对应的sockfd决定。我们接收到消息的时候对方也传入了其对应的IP和prot所以我们可以如下使用代码如下void Start() { char inbuffer[1024]; while (true) { struct sockaddr_in peer; socklen_t lensizeof(peer); ssize_t nrecvfrom(_socketfd,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)peer,len); if(n0) { inbuffer[n]0; LOG(LogLevel::DEBUG)cilent say#inbuffer; std::string echo_stringserver echo#; echo_stringinbuffer; sendto(_socketfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)peer,len); } else { LOG(LogLevel::ERROR)recvform error; } } }如上对于服务器端的设计就大致完成了。EchoClient.cc首先我们的客户端需要进行网络通信那么其也需要创建一个网络文件那么其也得进行网络文件的创建那么我们的用户端的socket就创建好了下面我们的用户端也要有其对应的IP和port这是在网络中标识自己的唯一标志。但是要注意的是我们对于客户端的bind不要去显示bind这是为啥呢因为对于服务端其未来是1n的情况那么我们服务端的端口号一般是固定的。但是对于用户端我们对其是多少并不关心其只需要存在就可以。并且未来用户端也会打开多个服务端那么如果显示绑定那么可能会出现端口号冲突的问题。所以对于用户端我们使用随机端口的方式由OS进行选择。而且我们发现日常使用软件的时候都是先给服务端进行数据发送所以OS底层会隐式自动进行获取随机端口然后进行bind IP和prot。下面我们完成对用户端的消息发送首先我们要获取到用户的输入但是有个问题我们得知道要进行通信的服务器所以这个要我们的用户进行输入所以在使用这个程序前我们要告诉用户输入对于服务器的IP和prot。但是有个问题就是我们用户又是如何知道对方的IP和prot的呢这个信息是内置到客户端中的是直接包含在内的。所以我们是直接进行硬编码的。上面我们就把发送方式还有发送的数据是啥搞定下面就是发送给谁了所以我们还需要一个struct socket_in 结构体变量。未来我们的发送方要将服务端的IP和prot也要发送给服务器所以其IP和prot也要转换为网络序列的。给服务器端发送完数据后那么就要接收服务器端发送来的数据了。下面我们来测试两个端口是否能够进行通信。首先我们先来进行本地通信那么在对于本地通信那么在输入IP的时候要设置为127.0.0.1然后对于端口号我们可以先随便设置下面我们设置为8080。上面我们使用的127.0.0.1IP地址其是本地环回地址其只是将数据在协议栈中走一遍不会经过网络。可以用于本地通信或者网络代码的测试。那么我们要通过网络进行数据交流又如何操作呢首先我们先补充下面几个问题1、云服务器的公网IP是禁止被显示bind的2、在服务器开发的时候不建议用户显示bind的IP地址3、云服务器的公网IP是禁止被bind的但是并不是不能被使用在我们的云服务器中是存在多个IP地址的如果我们的服务器bind了IP那么其未来只能和这个IP的机器进行通信了但是未来我们的服务器是要和多个IP的用户进行通信的所以这也是为啥不能显示bind其IP。所以我们是需要服务器和任意IP绑定主要其端口号一样即可。所以我们将服务器端的IP绑定的IP进行设置为INADDR_ANY。那么下面我们再来运行看看可以看到服务器端和用户端此时可以进行通信在我们的服务器端我们也要将用户端的IP和prot获取下来所以我们在收到用户数据的时候要在服务器端进行获取。未来我们的服务器是会被多个用户访问的所以我们可以将这些用户进行管理。下面我们来认识一个指令netstat那么其可以把我们当前机器中和网络和通信相关的信息打印出来。其有如下选项二、简单的英译汉的⽹络字典上面我们实现的是实现网络中进行通信但是没有任何的业务所以下面我们将上面的对于数据进行翻译处理。上面我们对于收到数据对于数据的处理也放在了服务器端那么下面我们想让服务器只负责数据的收发而数据的处理交给别的层次去进行。代码如下#pragma once #include iostream #include string #include unordered_map using namespace NS_LOG_MODULE; static const std::string default_dict ./dict.txt; static const std::string sep : ; // 分隔符 class Dict { public: Dict(const std::string dict_path default_dict) : _dict_path(dict_path) { LoadDict(); } ~Dict() {} void LoadDict() { std::ifstream in(_dict_path); if (!in.is_open()) { LOG(LogLevel::FATAL) open _dict_path error; exit(1); } std::string line; while (std::getline(in, line)) { LOG(LogLevel::FATAL) load: line success; auto pos line.find(sep); if (pos std::string::npos) { LOG(LogLevel::WARNING) Format: line success; continue; } std::string k line.substr(0, pos); std::string v line.substr(pos sep.size()); _dict.insert(std::make_pair(k, v)); } in.close(); } std::string Translate(std::string word) { auto iter _dict.find(word); if (iter ! _dict.end()) { return iter-second; } else { return None; } } private: std::string _dict_path; std::unordered_mapstd::string, std::string _dict; };三、多用户的服务器上面我们的服务器和用户端我们发现用户端都是单用户的所以下面我们可以通过多线程的方式实现。那么我们服务器端将用户进行管理起来多各个用户进行任何的收发。我们可以将前面写的线程池引入我们的服务器端将数据发送到线程池通过线程池进行任务的转发。后续我们服务端对于用户端的管理我们对于IP和prot进行管道来确定用户的唯一性。代码如下#pragma onec #include iostream #include string #include vector #include InetAddr.hpp #include Logger.hpp using namespace NS_LOG_MODULE; class UserManager { public: UserManager() { } void AddUser(const InetAddr addr) { if (SearchUser(addr)) { return; } _users.push_back(addr); } void DelUser(const InetAddr addr) { if (!SearchUser(addr)) { return; } for (auto iter _users.begin(); iter ! _users.end(); iter) { if (*iter addr) { _users.erase(iter); break; } } } bool SearchUser(const InetAddr addr) { for (auto users : _users) { if (users addr) { return true; } } return false; } bool ModUser(const InetAddr addr) { DelUser(addr); AddUser(addr); } ~UserManager() { } private: std::vectorInetAddr _users; };这是对多用户进行管理的部分。那么我们服务器的对于数据转发到我们的用户所以下面我们再设置一个路由转发的功能代码如下#pragma once #includeiostream #includestring #includememory #includesys/socket.h #includeMutex.hpp #includeUserManager.hpp class Route { public: Route() :_uma(std::unique_ptrUserManager()) { } void CheckUser(const InetAddr addr) { LockGuard lockguard(_lock); _uma-AddUser(addr); } void OfflineUser(const InetAddr addr) { LockGuard lockguard(_lock); _uma-DelUser(addr); } void Broadcast(int sockfd,std::string message) { LockGuard lockguard(_lock); auto users_uma-Users(); for(auto user:users) { sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)user.GetNetAddress(),user.Len()); } } ~Route(); private: std::unique_ptrUserManager _uma; Mutex _lock; };
返回列表