> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。> 目标:UDP网络服务器简单模拟实现。
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:网络
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
一、前言
前面我们已经学习了网络的基础知识,对网络的基本框架已有认识,算是初步认识到网络了,如果上期我们的学习网络是步入基础知识,那么这次学习的板块就是基础知识的实践,我们今天的板块是学习网络重要之一,学习完这个板块对虚幻的网络就不再迷茫!!!
二、主体
学习【网络】套接字编程——UDP通信咱们按照下面的图解:
2.1 概述
在使用TCP编写的应用程序和使用UDP编写的应用程序之间存在一些本质的差异,其原因在于这两个传输层之间的差别:UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。
以下给出了典型的UDP客户/服务器的函数调用。客户不与服务器建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须指定目的地(即服务器)的地址作为参数。类似的,服务器不接受来自客户端的连接,而是只管调用recvfrom函数,等待来自某个客户的数据到达。recvfrom将接受与所接受的数据报一道返回客户的协议地址,因此服务器可以把响应的发送给正确的客户。
2.2 Udp Server(服务端)
接下来接下来实现一批基于 UDP
协议的网络程序,本节只介绍基于IPv4的socket网络编程。
2.2.1 核心功能
说明:
分别实现客户端与服务器,客户端向服务器发送消息,服务器收到消息后,回响给客户端,有点类似于 echo
指令,该程序的核心在于 使用 socket
套接字接口,以 UDP
协议的方式实现简单网络通信。
2.2.2 核心结构
程序由server.hpp server.cc client.hpp client.cc 组成,大体框架如下:
创建 server.hpp
服务器头文件:
#pragma once#include <iostream>namespace nt_server
{class UdpServer{public:// 构造UdpServer(){} // 析构~UdpServer(){} // 初始化服务器void InitServer(){}// 启动服务器void StartServer(){}private:// 字段};
}
创建 server.cc
服务器源文件:
#include <memory> // 智能指针相关头文件
#include "server.hpp"using namespace std;
using namespace nt_server;int main()
{unique_ptr<UdpServer> usvr(new UdpServer());//使用智能指针创建了一个UdeServer对象// 初始化服务器usvr->InitServer();// 启动服务器usvr->StartServer();return 0;
}
创建 client.hpp
客户端头文件:
#pragma once#include <iostream>namespace nt_client
{class UdpClient{public:// 构造UdpClient() {} // 析构~UdpClient() {} // 初始化客户端void InitClient() {}// 启动客户端void StartClient() {}private:// 字段};
}
创建 client.cc
客户端源文件:
#include <memory>
#include "client.hpp"using namespace std;
using namespace nt_client;int main()
{unique_ptr<UdpClient> usvr(new UdpClient());// 初始化客户端usvr->InitClient();// 启动客户端usvr->StartClient();return 0;
}
创建 Makefile
文件:
.PHONY:all
all:server clientserver:server.ccg++ -o $@ $^ -std=c++11client:client.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -rf server client
2.2.3 Udp Server 端代码
2.2.3.1 socket - 创建套接字
语法:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int socket(int domain, int type, int protocol);
解释说明:
- domain:协议域(协议族)。该参数决定了 socket 的地址类型。在通信中必须采用对应的地址,如 AF_INET 决定了要用 IPv4 地址(32位的)与端口号(16位)的组合,AF_UNIX 决定了要用一个绝对路径名作为地址,AF_INET6(IPv6)。
- type:指定了 socket 的类型,如 SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报式套接字)等等。
- protocol:指定协议,如 IPPROTO_TCP(TCP传输协议)、PPTOTO_UDP(UDP 传输协议)、IPPROTO_SCTP(STCP 传输协议)、IPPROTO_TIPC(TIPC 传输协议)。
- 返回值:一个文件描述符,创建套接字的本质其实就是打开一个文件。
功能说明:
- socket函数打开一个网络通讯端口,如果成功的话就像open一样返回一个文件描述符,应用程序可以像读写文件一样read/write在网络上收发数据。
- 好了socket函数学完了,接下来在 server.hpp 的 InitServer() 函数中创建套接字,并对创建成功/失败后的结果做打印。
代码呈现:
#pragma once#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>namespace nt_server
{// 错误码enum{SOCKET_ERR = 1};class UdpServer{public:// 构造UdpServer(){} // 析构~UdpServer(){} // 初始化服务器void InitServer(){// 1.创建套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);if(sock_ == -1)//创建失败{std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 创建成功std::cout << "Create Success Socket: " << sock_ << std::endl;}// 启动服务器void StartServer(){}private:int sock_; // 套接字};
}
总结说明:
因为这里是使用 UDP 协议实现的 网络通信,参数1 domain 选择 AF_INET(基于 IPv4 标准),参数2 type 选择 SOCK_DGRAM(数据报传输),参数3设置为 0,可以根据 SOCK_DGRAM 自动推导出使用 UDP 协议。
2.2.3.2 bind - 将套接字与一个 IP 和端口号进行绑定
语法:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
解释说明:
- sockfd: 通过socket函数得到的文件描述符
- addr: 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen: 第二个参数结构体占的内存大小
详细了解一下这个sockaddr_in结构体:
struct sockaddr_in {short sin_family; // 2 字节 ,地址族,e.g. AF_INET, AF_INET6unsigned short sin_port; // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),struct in_addr sin_addr; // 4 字节 ,32位IP地址char sin_zero[8]; // 8 字节 ,不使用
};
struct in_addr {unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
};
获得一个干净可用的 sockaddr_in 结构体后,可以正式绑定 IP 地址 和 端口号了。
代码呈现:
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>namespace nt_server
{// 退出码enum{SOCKET_ERR = 1,BIND_ERR};// 端口号默认值const uint16_t default_port = 8888;class UdpServer{public:// 构造UdpServer(const std::string ip, const uint16_t port = default_port):port_(port), ip_(ip){} // 析构~UdpServer(){} // 初始化服务器void InitServer(){// 1.创建套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);if(sock_ == -1){std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 创建成功std::cout << "Create Success Socket: " << sock_ << std::endl;// 2.绑定IP地址和端口号struct sockaddr_in local;bzero(&local, sizeof(local)); // 置0// 填充字段local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)local.sin_port = htons(port_); // 主机序列转为网络序列local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列// 绑定IP地址和端口号int n = bind(sock_, (const sockaddr*)&local, sizeof(local));if(n<0){std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;exit(BIND_ERR);}// 绑定成功std::cout << "Bind IP&&Port Success" << std::endl;}// 启动服务器void StartServer(){}private:int sock_; // 套接字uint16_t port_; // 端口号std::string ip_; // IP地址(后面需要删除)};
}
总结说明:
- 端口号会在网络里互相转发,需要把主机序列转换为网络序列,可以使用 htons 函数。
- 需要把点分十进制的字符串,转换为无符号短整数,可以使用 inet_addr 函数,这个函数在进行转换的同时,会将主机序列转换为网络序列(因为IP地址需要在网络里面发送)。
- 绑定IP地址和端口号这个行为并非直接绑定到当前主机中,而是在当前程序中,将创建的 socket 套接字,与目标IP地址与端口号进行绑定,当程序终止后,这个绑定关系也会随之消失。
2.2.3.3 地址转换函数 - 字符串和struct in_addr互相转换
我们这里为什么要使用字符串来表示IP地址:
首先大部分用户习惯使用的IP是点分十进制的字符串,就像下面这个这样。
128.11.3.31
我们的IP地址就是下面的第3个成员:
struct sockaddr_in {short sin_family; // 2 字节 ,地址族,e.g. AF_INET, AF_INET6unsigned short sin_port; // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),struct in_addr sin_addr; // 4 字节 ,32位IP地址char sin_zero[8]; // 8 字节 ,不使用
};
struct in_addr {unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
};
点分十进制的IP地址不好输入,我们往往先用更好输入的字符串来存储IP地址,然后将字符串版的IP地址转换为struct in_addr版的IP地址(也就是点分十进制版的)。
字符串转 in_addr
结构体:
语法:
#include <arpa/inet.h>in_addr_t inet_addr(const char *cp);
说明:
- 该函数将点分十进制的字符串表示的IPv4地址转换为网络字节序的32位整数。返回的是in_addr_t类型,通常用于填充sin_addr.s_addr字段。
- 这个函数是更通用的函数,支持IPv4和IPv6地址的转换。第一个参数 af 表示地址族,常用的是 AF_INET(IPv4)和 AF_INET6(IPv6)。第二个参数 src 是输入的字符串表示的IP地址,第三个参数 dst 是输出的二进制表示的IP地址。
使用:
#include <arpa/inet.h>struct in_addr ipv4Address;
const char *ipString = "192.168.1.1";
inet_pton(AF_INET, ipString, &(ipv4Address.s_addr));
in_addr 结构体转字符串:
语法:
#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);
说明:
-
第一个参数 af 表示地址族,常用的是 AF_INET(IPv4)和 AF_INET6(IPv6)。
-
第二个参数 src 是输入的二进制表示的IP地址。
-
第三个参数 dst 是输出的字符串表示的IP地址的缓冲区。第四个参数 size 是缓冲区的大小。
使用:
#include <arpa/inet.h>struct in_addr ipv4Address;
ipv4Address.s_addr = inet_addr("192.168.1.1");
char ipString[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(ipv4Address.s_addr), ipString, INET_ADDRSTRLEN);
我们让IP绑定到 0.0.0.0,0.0.0.0
表示任意IP地址:
#pragma once
//.....
namespace nt_server
{// 退出码enum{SOCKET_ERR = 1,BIND_ERR};// 端口号默认值const uint16_t default_port = 8888;const std::string="0.0.0.0";//注意这里class UdpServer{public:// 构造UdpServer(const std::string ip=defaultip, const uint16_t port = default_port):port_(port), ip_(ip){} // 析构~UdpServer(){} // 初始化服务器void InitServer(){//。。。。// 2.绑定IP地址和端口号struct sockaddr_in local;bzero(&local, sizeof(local)); // 置0// 填充字段local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)local.sin_port = htons(port_); // 主机序列转为网络序列local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列//。。。。}// 启动服务器void StartServer(){}private:int sock_; // 套接字uint16_t port_; // 端口号std::string ip_; // IP地址};
}
server.cc
服务器源文件:
#include <memory> // 智能指针相关头文件
#include "server.hpp"using namespace std;
using namespace nt_server;int main()
{unique_ptr<UdpServer> usvr(new UdpServer());// 初始化服务器usvr->InitServer();// 启动服务器usvr->StartServer();return 0;
}
2.2.3.3 recvfrom - 从服务器的套接字里读取数据
语法:
#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);
解释说明:
- buf:接收缓冲区。
- len:接收缓冲区的大小。
- flags:默认设置为 0,表示阻塞。
- src_addr:输出型参数,获取客户端的套接字信息,也就是获取客户端的 ip 和端口号信息。因为是 udp 网络通信,所以这里传入的还是 struct sockaddr_in 类型的对象地址。
- addrlen:这里就是 struct sockaddr_in 对象的大小。
- 返回值:成功会返回获取到数据的字节数;失败返回 -1。
使用示例:
struct sockaddr_in sender;
socklen_t sender_len = sizeof(sender);
char buffer[1024];int bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,(struct sockaddr*)&sender, &sender_len);
if (bytes_received < 0) {perror("recvfrom failed");// handle error
}
代码呈现:server.hpp
服务器头文件
namespace nt_server
{// 退出码enum{SOCKET_ERR = 1,BIND_ERR};// 端口号默认值const uint16_t default_port = 8888;class UdpServer{//.....// 启动服务器void StartServer(){// 服务器是不断运行的,所以需要使用一个 while(true) 死循环char buff[1024]; // 缓冲区while (true){// 1. 接收消息struct sockaddr_in peer; // 客户端结构体socklen_t len = sizeof(peer); // 客户端结构体大小// 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'// 传入 0 表示当前是阻塞式读取ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0)buff[n] = '\0';elsecontinue; // 继续读取// 2.处理数据std::string clientIp = inet_ntoa(peer.sin_addr); // 获取IP地址uint16_t clientPort = ntohs(peer.sin_port); // 获取端口号printf("Server get message from [%s:%d]$ %s\n", clientIp.c_str(), clientPort, buff);// 3.回响给客户端// ...}}//.....};
}
2.2.3.4 sendto - 向指定套接字中发送数据
语法:
#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: struct sockaddr_in 对象的大小。
使用示例:
struct sockaddr_in receiver;
receiver.sin_family = AF_INET;
receiver.sin_port = htons(12345); // Some port number
inet_pton(AF_INET, "192.168.1.1", &receiver.sin_addr); // Some IP addresschar message[] = "Hello, World!";
ssize_t bytes_sent = sendto(sockfd, message, sizeof(message), 0,(struct sockaddr*)&receiver, sizeof(receiver));
if (bytes_sent < 0) {perror("sendto failed");// handle error
}
代码呈现:server.hpp
服务器头文件
//。。。
namespace nt_server
{// 退出码enum{SOCKET_ERR = 1,BIND_ERR};// 端口号默认值const uint16_t default_port = 8888;class UdpServer{//.....// 启动服务器void StartServer(){// 服务器是不断运行的,所以需要使用一个 while(true) 死循环char buff[1024]; // 缓冲区while (true){// 1. 接收消息struct sockaddr_in peer; // 客户端结构体socklen_t len = sizeof(peer); // 客户端结构体大小// 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'// 传入 0 表示当前是阻塞式读取ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0)buff[n] = '\0';elsecontinue; // 继续读取// 2.处理数据std::string clientIp = inet_ntoa(peer.sin_addr); // 获取IP地址uint16_t clientPort = ntohs(peer.sin_port); // 获取端口号printf("Server get message from [%c:%d]$ %s\n", clientIp.c_str(), clientPort, buff);// 3.回响给客户端n = sendto(sock_, buff, strlen(buff), 0, (const struct sockaddr *)&peer, sizeof(peer));if (n == -1)std::cout << "Send Message Fail: " << strerror(errno) << std::endl;}}//.....};
}
如何证明服务器端正在运行:
可以通过 Linux
中查看网络状态的指令,因为我们这里使用的是 UDP
协议,所以只需要输入下面这条指令,就可以查看有哪些程序正在运行
netstat -nlup
修改 sever.cc 代码:
#include <memory> // 智能指针相关头文件
#include "server.hpp"using namespace std;
using namespace nt_server;int main()
{unique_ptr<UdpServer> usvr(new UdpServer(80));//使用智能指针创建了一个UdeServer对象// 初始化服务器usvr->InitServer();// 启动服务器usvr->StartServer();return 0;
}
2.2.3.5 命令行参数改装服务端
上面的代码中,我们的端口号都是在代码里面指定了的,但是我们不能每次使用的时候都去修改代码吧,我们其实通过命令行参数来指定端口号
server.hpp:
//....// 退出码enum{SOCKET_ERR = 1,BIND_ERR,USAGE_ERR};
//.....
server.cc:
#include <memory> // 智能指针相关头文件
#include "server.hpp"using namespace std;
using namespace nt_server;void Usage(const char* program)
{cout << "Usage:" << endl;cout << "\t" << program << " ServerPort" << endl;
}int main(int argc, char* argv[])
{if (argc != 2){// 错误的启动方式,提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串,我们需要将其转换成对应的类型uint16_t port = stoi(argv[1]);//将字符串转换成端口号unique_ptr<UdpServer> usvr(new UdpServer(port));//使用智能指针创建了一个UdeServer对象// 初始化服务器usvr->InitServer();// 启动服务器usvr->StartServer();return 0;
}
2.3 Udp Client 客户端
因为一个端口号只能被一个进程 bind,客户端的应用是非常多的,如果在客户端采用静态 bind,那可能会出现两个应用同时 bind 同一个端口号,此时就注定了这两个应用一定是不能同时运行的。为了解决这个问题,一般不建议客户端 bind 一个固定的端口,而是由操作系统来进行动态的 bind,这样就可以避免端口号发生冲突。这也间接说明,对一个 client 端的进程来说,它的端口号是几并不重要,只要能够标识该进程在主机上的唯一性就可以。因为,一般都是由 clinet 端主动的向 server 端发送消息,所以 client 一定是能够知道 client 端的端口号。相反,服务器的端口号必须是确定的。因此,在编写客户端的代码时,第一步就是创建套接字,创建完无需进行 bind,直接向服务器发送数据,发送的时候,操作系统会为我们进行动态 bind。
client.hpp代码:
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>namespace nt_client
{// 退出码enum{SOCKET_ERR = 1,BIND_ERR,USAGE_ERR};class UdpClient{public:// 构造UdpClient(const std::string& ip, uint16_t port):server_ip_(ip), server_port_(port){} // 析构~UdpClient() {} // 初始化客户端void InitClient() {// 1.创建套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);if(sock_ == -1){std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}std::cout << "Create Success Socket: " << sock_ << std::endl;// 2.构建服务器的 sockaddr_in 结构体信息bzero(&svr_, sizeof(svr_));svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址svr_.sin_port = htons(server_port_); // 绑定服务器端口号}// 启动客户端
void StartClient()
{char buff[1024];while(true){// 1.发送消息std::string msg;std::cout << "Input Message# ";std::getline(std::cin, msg);ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));if(n == -1){std::cout << "Send Message Fail: " << strerror(errno) << std::endl;continue; // 重新输入消息并发送}// 2.接收消息socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);if(n > 0)buff[n] = '\0';elsecontinue;// 可以再次获取IP地址与端口号std::string ip = inet_ntoa(svr_.sin_addr);uint16_t port = ntohs(svr_.sin_port);printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);}
}private:std::string server_ip_; // 服务器IP地址uint16_t server_port_; // 服务器端口号int sock_;struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息};
}
client.cc代码:
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>namespace nt_client
{// 退出码enum{SOCKET_ERR = 1,BIND_ERR,USAGE_ERR};class UdpClient{public:// 构造UdpClient(const std::string& ip, uint16_t port):server_ip_(ip), server_port_(port){} // 析构~UdpClient() {} // 初始化客户端void InitClient() {// 1.创建套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);if(sock_ == -1){std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}std::cout << "Create Success Socket: " << sock_ << std::endl;// 2.构建服务器的 sockaddr_in 结构体信息bzero(&svr_, sizeof(svr_));svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址svr_.sin_port = htons(server_port_); // 绑定服务器端口号}// 启动客户端
void StartClient()
{char buff[1024];while(true){// 1.发送消息std::string msg;std::cout << "Input Message# ";std::getline(std::cin, msg);ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));if(n == -1){std::cout << "Send Message Fail: " << strerror(errno) << std::endl;continue; // 重新输入消息并发送}// 2.接收消息socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);if(n > 0)buff[n] = '\0';elsecontinue;// 可以再次获取IP地址与端口号std::string ip = inet_ntoa(svr_.sin_addr);uint16_t port = ntohs(svr_.sin_port);printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);}
}private:std::string server_ip_; // 服务器IP地址uint16_t server_port_; // 服务器端口号int sock_;struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息};
}
2.4 简易公共聊天室
server.hpp代码:
#pragma once#include <iostream>
#include <string>
#include <functional>//注意这个
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<unordered_map>namespace nt_server
{// 退出码enum{SOCKET_ERR = 1,BIND_ERR,USAGE_ERR};// 端口号默认值const uint16_t default_port = 8888;using func_t = std::function<std::string(std::string)>;
// 可以简单的理解为func_t是一个参数为string,返回值同样为string的函数的类型class UdpServer{public:// 构造UdpServer(const func_t& func, uint16_t port = default_port)//注意这里的func_t:port_(port),serverHandle_(func)
//注意serverHandle_的类型已经是一个func_t,就是一个一个参数为string,返回值同样为string的函数的类型{}// 析构~UdpServer(){} // 初始化服务器void InitServer(){// 1.创建套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);if(sock_ == -1){std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 创建成功std::cout << "Create Success Socket: " << sock_ << std::endl;// 2.绑定IP地址和端口号struct sockaddr_in local;bzero(&local, sizeof(local)); // 置0// 填充字段local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)local.sin_port = htons(port_); // 主机序列转为网络序列// local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列local.sin_addr.s_addr = INADDR_ANY; // 绑定任何可用IP地址// 绑定IP地址和端口号if(bind(sock_, (const sockaddr*)&local, sizeof(local))){std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;exit(BIND_ERR);}// 绑定成功std::cout << "Bind IP&&Port Success" << std::endl;}//检测是不是新用户void CheckUser(const struct sockaddr_in & client,const std::string clientIp_,uint16_t clientPort_){auto iter= online_user_.find(clientIp_);if(iter==online_user_.end()){online_user_.insert({clientIp_,client});std::cout<<"["<<clientIp_<<":"<<clientPort_<<"] add to oniline user."<<std::endl;}}//广播给所有人void Broadcast(const std::string& respond,const std::string clientIp_,uint16_t clientPort_){for(const auto&usr :online_user_){std::string message="[";message+=clientIp_;message+=" : ";message+=std::to_string(clientPort_);message+="]#";message+=respond;int z = sendto(sock_, respond.c_str(), respond.size(), 0, (const sockaddr*)&usr.second, sizeof(usr.second));if(z == -1)std::cout << "Send Message Fail: " << strerror(errno) << std::endl;}}// 启动服务器void StartServer(){// 服务器是不断运行的,所以需要使用一个 while(true) 死循环char buff[1024]; // 缓冲区while(true){// 1. 接收消息struct sockaddr_in client; // 客户端结构体socklen_t len = sizeof(client); // 客户端结构体大小// 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'// 传入 0 表示当前是阻塞式读取ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&client, &len);if(n > 0)buff[n] = '\0';elsecontinue; // 继续读取// 2.处理数据std::string clientIp = inet_ntoa(client.sin_addr); // 获取用户的IP地址uint16_t clientPort = ntohs(client.sin_port); // 获取端口号//2.1.判断是不是新用户,如果是就加入,如果不是就什么也不做CheckUser(client,clientIp,clientPort); //2.2 对数据进行业务处理,并获取业务处理后的结果std::string respond = serverHandle_(buff);//特别注意这里,业务处理的代码已经放到了这个serverHandle_这个函数了,// 3.回响给所有在线客户端Broadcast(respond,clientIp,clientPort); }}private:int sock_; // 套接字uint16_t port_; // 端口号func_t serverHandle_; // 业务处理函数(回调函数)std::unordered_map<std::string,struct sockaddr_in> online_user_;//在线用户列表};
}
server.cc代码:
#include <string>
#include <vector>
#include <memory> // 智能指针相关头文件
#include <cstdio>
#include "server.hpp"using namespace std;
using namespace nt_server;//业务处理函数
std::string ExecCommand(const std::string& request)
{return request;
}void Usage(const char* program)
{cout << "Usage:" << endl;cout << "\t" << program << " ServerPort" << endl;
}int main(int argc, char* argv[])
{if (argc != 2){// 错误的启动方式,提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串,我们需要将其转换成对应的类型uint16_t port = stoi(argv[1]);//将字符串转换成端口号unique_ptr<UdpServer> usvr(new UdpServer(ExecCommand,port));// 初始化服务器usvr->InitServer();// 启动服务器usvr->StartServer();return 0;
}
client.hpp代码:
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include<functional>namespace nt_client
{// 退出码enum{SOCKET_ERR = 1,BIND_ERR,USAGE_ERR};class UdpClient{public:// 构造UdpClient(const std::string &ip, uint16_t port): server_ip_(ip), server_port_(port){}// 析构~UdpClient(){}static void *send_message(void *argc)//传进来的是this指针{UdpClient*_this =(UdpClient*)argc;//强制转换为类指针char buff[1024];while (1){// 1.发送消息std::string msg;std::cout << "Input Message# ";std::getline(std::cin, msg);ssize_t n = sendto(_this->sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr *)&_this->svr_, sizeof(_this->svr_));if (n == -1){std::cout << "Send Message Fail: " << strerror(errno) << std::endl;continue; // 重新输入消息并发送}}return (void*)0;}static void *recv_message(void *argc)//传进来的是this指针{UdpClient*_this =(UdpClient*)argc;char buff[1024];while (1){// 2.接收消息socklen_t len = sizeof(_this->svr_); // 创建一个变量,因为接下来的参数需要传左值int n = recvfrom(_this->sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&_this->svr_, &len);if (n > 0)buff[n] = '\0';elsecontinue;// 可以再次获取IP地址与端口号std::string ip = inet_ntoa(_this->svr_.sin_addr);uint16_t port = ntohs(_this->svr_.sin_port);printf("Client get message from [%s:%d]# %s\n", ip.c_str(), port, buff);}return (void*)0;}// 初始化客户端void InitClient(){// 1.创建套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);if (sock_ == -1){std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}std::cout << "Create Success Socket: " << sock_ << std::endl;// 2.构建服务器的 sockaddr_in 结构体信息bzero(&svr_, sizeof(svr_));svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址svr_.sin_port = htons(server_port_); // 绑定服务器端口号}// 启动客户端void StartClient(){pthread_t recv, sender;//只需将类内的线程函数变化为static函数,但是static函数就不能直接访问到我们类内的私有数据,//不要忘了这个线程函数的void*参数,我们可以把this指针作为参数传给它,//然后就能通过这个this指针来访问到我们的类内的私有数据pthread_create(&recv, nullptr, recv_message, (void*)this);pthread_create(&sender, nullptr, send_message, (void*)this);pthread_join(recv,nullptr);pthread_join(sender,nullptr);}private:std::string server_ip_; // 服务器IP地址uint16_t server_port_; // 服务器端口号int sock_;//套接字描述符struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息};
}
client.cc保持不变
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>namespace nt_client
{// 退出码enum{SOCKET_ERR = 1,BIND_ERR,USAGE_ERR};class UdpClient{public:// 构造UdpClient(const std::string& ip, uint16_t port):server_ip_(ip), server_port_(port){} // 析构~UdpClient() {} // 初始化客户端void InitClient() {// 1.创建套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);if(sock_ == -1){std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}std::cout << "Create Success Socket: " << sock_ << std::endl;// 2.构建服务器的 sockaddr_in 结构体信息bzero(&svr_, sizeof(svr_));svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址svr_.sin_port = htons(server_port_); // 绑定服务器端口号}// 启动客户端
void StartClient()
{char buff[1024];while(true){// 1.发送消息std::string msg;std::cout << "Input Message# ";std::getline(std::cin, msg);ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));if(n == -1){std::cout << "Send Message Fail: " << strerror(errno) << std::endl;continue; // 重新输入消息并发送}// 2.接收消息socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);if(n > 0)buff[n] = '\0';elsecontinue;// 可以再次获取IP地址与端口号std::string ip = inet_ntoa(svr_.sin_addr);uint16_t port = ntohs(svr_.sin_port);printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);}
}private:std::string server_ip_; // 服务器IP地址uint16_t server_port_; // 服务器端口号int sock_;struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息};
}
makefile代码:
.PHONY:all
all:server clientserver:server.ccg++ -o $@ $^ -std=c++11 -lpthreadclient:client.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -rf server client
三、结束语
今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。