二级建造师最好的网站,商城网站建设需要,推广公司名字,哈尔滨公告前言
前面我们对网络的发展#xff0c;网络的协议、网路传输的流程做了介绍#xff0c;最后#xff0c;我们还介绍了 IP 和 端口号#xff0c;ip port 叫做 套接字 socket#xff0c; 本期我们就来介绍UDP套接字编程#xff01;
目录
1、预备知识
1.1 传输层协议: T…前言
前面我们对网络的发展网络的协议、网路传输的流程做了介绍最后我们还介绍了 IP 和 端口号ip port 叫做 套接字 socket 本期我们就来介绍UDP套接字编程
目录
1、预备知识
1.1 传输层协议: TCP/UDP
1.2 网络字节序
1.3 socket 接口
1.4 sockaddr
2、echo_server
2.1 核心功能分析
2.2 服务端设计
2.2.1 创建套接字
2.2.2 绑定ip和端口号
2.2.3 启动服务
2.2.4 服务端的全部源码
2.3 客户端设计
2.3.1 初始化客户端
2.3.2 启动客户端
2.3.3 客户端全部源码
2.3.4 解决遗留问题
2.4 优化和验证 3、简单的英译汉
3.1 实现Dict 类
3.2 完成服务端的修改
3.3 主函数完善
4、多人聊天室
4.1 服务端改造
4.1.1 Route类实现
4.1.2 服务端主函数修改
4.2 客户端改造
4.2.1 客户端的主函数改造
5、地址函数补充
5.1 字符串转整数
5.2 整数转字符串 1、预备知识
1.1 传输层协议: TCP/UDP
前面我们结合系统了解了网络协议栈是基于OS的而我们知道传输层是属于内核的那么要通过网络协议栈进行通信必定要调用系统调用 简单认识 TCP
TCP (Transmission Control Protocol) 传输控制协议它是一个传输层协议特点有 • 面向连接 • 可靠传输 • 面向字节流 这里简单的了解一下就可以后面会详细介绍它的工作原理和机制的例如常考的三次握手、四次挥手
简单认识 UDP
UDP User Datagram Protocol用户数据报协议它也是一个传输层协议特点有 • 无连接 • 不可靠传输 • 面向数据报 关于 可靠性TCP 的可靠传输并不意味着它可以将数据百分百递达而是说它在数据传输过程中如果发生了传输失败的情况它会通过自己独特的机制重新发送数据确保对端百分百能收到数据至于 UDP 就不一样数据发出后如果失败了也不会进行重传好在 UDP 面向数据报并且没有很多复杂的机制所以传输速度很快
总结起来就是TCP 用于对数据传输要求较高的领域比如金融交易、网页请求、文件传输等至于 UDP 可以用于短视频、直播、即时通讯等对传输速度要求较高的领域 如果不知道该使用哪种协议优先考虑 TCP如果对传输速度又要求可以选择 UDP 1.2 网络字节序
我们知道在计算机系统中多字节数据的存储方式有大端Big-Endian和小端Little-Endian之分。这种区别不仅存在于内存中还影响到磁盘文件和网络数据流的字节序。
内存中的字节序
• 大端字节序高字节MSB存储在低的内存地址上。
• 小端字节序低字节LSB存储在低的内存地址上。
网络数据流的字节序
• 发送过程发送主机按照内存地址从低到高的顺序将数据发出。即先发送的数据存储在发送缓冲区的低地址后发送的数据存储在高地址。
• 接收过程接收主机从网络上接收到的字节依次保存在接收缓冲区中也是按照内存地址从低到高的顺序。
• 因此网络数据流的地址是主机先发出的数据是低地址主机后发出的数据室高地址
• TCP/IP 协议为了确保不同架构的主机之间能够正确解析数据规定网络数据流采用大端字节序即 低地址高字节
• 所以不管当前主机是大端还是小端网络收发数据都必须要使用大端字节序
关于大小端我们在《C语言数据在内存中的存储》中详细的介绍过 那现在的问题是如果我当前的机子是大端机器那还好。但是如果是小端呢我是不是还得自己手动的转换 OK你想到的人家设计网络的人也是想到了为了让网络程序具有可移植性使用同样的C语言代码在大端和小端在计算机上编译后都能正常运行所以就提供了网络序列和主机序列的转换函数
#include arpa/inet.h// 主机转网络
uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);// 网络转主机
uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);这写函数的 h 表示 hostn 表示 network l 表示 32 位长整数s 表示 16 位 短整数。
如果主机是大端机器那就不做任何转换直接返回即可如果是小端机器转为大端然后返回
1.3 socket 接口
socket 套接字提供了下面这一批常用接口用于实现网络通信
#include sys/types.h
#include sys/socket.h// 创建socket文件描述符TCP/UDP 服务器/客户端
int socket(int domain, int type, int protocol);// 绑定端口号TCP/UDP 服务器
int bind(int socket, const struct sockaddr* address, socklen_t address_len);// 开始监听socket (TCP 服务器)
int listen(int socket, int backlog);// 接收连接请求 (TCP 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);// 建立连接 (TCP 客户端)
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);可以看到在这一批 API 中频繁出现了一个结构体类型 sockaddr该结构体支持网络通信也支持本地通信 socket 套接字就是用于描述 sockaddr 结构体的字段复用了文件描述符的解决方案 1.4 sockaddr
socket 这套网络通信标准隶属于 POSIX 通信标准该标准设计的初衷是为了实现 可移植性程序可以直接在使用该标准的不同机器中运行但是有的是网络通信有的是本地通信 socket 套接字为了能同时兼顾这两种通信方式 提供了 sockaddr 结构体
由 sockaddr 结构体衍生出了两个不同的结构体sockaddr_in 网络套接字 sockaddr_un 域间套接字 前者是网络通信 后者是本地通信 • 后面可以提取 sockaddr 的头部的 16 位地址类型判断是网络通信还是本地通信
• 在进行网络通信时需要提供 ip 地址 端口号 等而本地通信时只需要提供一个路径名即可通过读写同一个文件的形式进行通信类似于命名管道
• socket 提供的接口参数为 sockaddr*类型我们既可以传入 sockaddr_in 进行网络通信也可以传入 sockaddr_un 进行本地通信传参时将参数进行强制类型转换即可这是使用 C语言 实现 多态 的典型做法确保该标准的通用性 为什么不将参数设置为 void* 因为在该标准设计时C语言还不支持 void* 这种类型为了确保向前兼容性即便后续支持后也不能进行修改了 关于 socketaddr_in 结构和上述 socket API 的更多详细信息放到后面写代码时再细谈 2、echo_server
接下来我们将实现三个基于UDP的网络程序分别是字符串回响、简易的汉译英、多人聊天室
2.1 核心功能分析
分别实现客户端和服务端客户端向服务端发送请求服务端接收到请求之后直接回响给客户端和我们之前介绍的 echo 指令 类似 所以我们还是基于上面的先来搭建一个框架出来 UdpServer.hpp #pragma once#include iostreamclass UdpServer
{
public:// 构造UdpServer(){}// 初始化服务器void InitServer(){}// 启动服务器void Start(){}// 析构~UdpServer(){}
private://属性字段
}; UdpServerMain.cc 无论在服务端还是在客户端Main函数都将采用智能指针管理资源 #include UdpServer.hpp
#include memoryint main()
{std::unique_ptrUdpServer usvr std::make_uniqueUdpServer();// C14usvr-InitServer();usvr-Start();return 0;
} UdpClient.hpp #pragma once#include iostreamclass UdpClient
{
public:// 构造UdpClient(){}// 初始化客户端void UdpClient(){}// 启动客户端void Start(){}// 析构~UdpClient(){}
private:// 属性字段
}; UdpClientMain.cc #include UdpClient.hpp
#include memoryint main()
{std::unique_ptrUdpClient uclt std::make_uniqueUdpClient();// C14uclt-InitClient();uclt-Start();return 0;
} Makefile .PHONY: all
all : udpserver udpclientudpserver : UdpServerMain.ccg -o $ $^ -stdc14
udpclient : UdpClientMain.ccg -o $ $^ -stdc14.PHONY:clean
clean:rm -rf udpserver udpclient
2.2 服务端设计
服务端做的事情无非三个 1、接受客户端的请求 2、处理客户端请求 3、响应给客户端 上面的三个操作对应着的就是接收消息、处理消息、发送消息。如实现当然是使用 socket 套接字接口喽
2.2.1 创建套接字
创建套接字使用的是 socket 函数
#include sys/types.h
#include sys/socket.h// 创建套接字TCP/UDP 服务器/客户端
int socket(int domain, int type, int protocol);参数 • domain 创建套接字用于哪一种通信网络/本地 • type 选择数据传输类型流式/数据报 • protocol 选择协议类型支持根据type自动推到所以一般直接写0 返回值 成功返回一个文件描述符套接字失败返回-1 我们这里因为是UDP协议实现的网络通信所以参数 domain 选择 AF_INET基于IPv4标准参数2 type 选择 SOCK_DGRAM数据报传输参数3 protocol 协议直接设置为 0他会根据参数2自动推导 我们可以在服务端的初始化函数中创建套接字为了代码的可读性我们在引入我们的日志其次为了不让我们的服务端进行拷贝我们可以把拷贝构造和赋值拷贝给禁用掉也可以单独写一个类继承下来建议最后因为后面存在大量的判断退出的情况我们把退出的码单独使用枚举放在一个Common.hpp中后面谁用直接引用即可 nocopy.hpp class nocopy
{
public:nocopy(){}~nocopy(){}nocopy(const nocopy) delete;const nocopy operator(const nocopy) delete;
};
这样写的好处就是后面可以直接复用 Common.hpp #pragma onceenum
{SOCKET_ERROR 1
};
目前公共的这里没有啥只有一个 socket 的创建错误后面了会加
#pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include unistd.h#include nocopy.hpp
#include Common.hpp
#include Log.hppusing namespace LogModule;class UdpServer : public nocopy
{
public:// 构造UdpServer(){}// 初始化服务器void InitServer(){// 创建 socket 套接字_sockfd ::socket(AF_INET, SOCK_DGRAM, 0);// IPv4 面向数据报if(_sockfd 0){LOG(FATAL, socket create failed\n);exit(SOCKET_ERROR);}LOG(INFO, socket create success, sockfd is %d\n, _sockfd);// 预期 3}// 启动服务器void Start(){}// 析构~UdpServer(){}
private:int _sockfd;
};
我们来编译一下此时 sockfd 应该是3因为 【文件描述符】那里介绍过0、1、2被占用了 没有问题这也证明了 套接字的本质就是 文件描述符不过它用于描述网络资源
2.2.2 绑定ip和端口号
两台主机通信的本质是两台主机上的两个进程在通信即进程间通信。
• 如何在网络中标识不同主机IP地址
• 网络中如何在一台主机上标识唯一的一个进程端口号
所以我们只要知道对方的 ipport 就可以唯一在网络中确定一个进程然后就可以通信了所以我们在客户端和服务端都需要进行绑定 ipport 注意目前这里我的是云服务器云服务器不建议绑定一个特定的ip而客户端也是不用用户显示的绑定的OS自动绑定 这一点后面解释 使用 bind 函数进行绑定
#include sys/types.h
#include sys/socket.h// 绑定IP地址和端口号TCP/UDP 服务器
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);参数 • sockfd 创建成功的套接字 • addr 通信信息的结构体 • addrlen 通信信息结构体的大小 这里第一个参数就是上面刚刚创建 socket 成功的返回值主要介绍的是第二个 addr
上面说了, socket 套接字通信标准为了兼顾 网络通信 和 本地通信 所以提供了 struct sockaddr 而实际网络通信用的是 struct sockaddr_in本地通信用的是 struct sockaddr_un这里是基于 UDP的网络通信所以我们重点关注一下 struct sockaddr_in
网络通信本质是进程间通信而ip标识不同网络中的主机port标识主机中的不同进程所以双方通信前得知道对方的ip和port而它两就会存储在struct sockaddr_in 中下面这就是 sockaddr_in 结构体中的字段
struct sockaddr_in
{__SOCKADDR_COMMON(sin_);in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of struct sockaddr. */unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)];
}; __SOCKADDR_COMMON(sin_);
这就是一个 短整数16位标识是 网络通信 还是 本地通信这里其实就是一个宏他的原型如下
typedef unsigned short int sa_family_t;/* This macro is used to declare the initial common membersof the data types used for socket addresses, struct sockaddr,struct sockaddr_in, struct sockaddr_un, etc. */#define __SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int)) 其中 sa_family_t 本质就是一个 短整数而 sa_prefix 就是我们传递的通信类型即 sin_网络/本地这里发现 sa_prefix 和 family 给用 ## 连接了这在C语言介绍过这表示将传入的 sin_ 和 family 拼接为一个新的类型 sa_family_t 即标识通信类型/方式的16位 sin_port 就是端口号其中 in_port_t 就是 uint16_t 的短整数
typedef uint16_t in_port_t;
sin_addr 表示的是 IP 地址他这里又是一个结构体 in_addr 类型的变量 其实这里面就一个整型变量
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};
这里明显的看到IPv4的时候是一个32位的整数也就是说网络序列中存储ip使用的是一个整数此时你肯定好奇不对呀我平时用的是这种127.0.0.1的字符串啊其实这种叫做 点分十进制方便用户看真实的网络序列采用的是上面的整数存储的 那是不是我也需要把这种点分十进制的字符串手动的转成整数呢 是的但是不需要你自己写函数转人家已经写好了例如inet_addr 还有其他的我们最后会补充的目前暂时用这个就OK 介绍到这里我们就可以将 sockfd 和 存储 ip 和 port 信息的结构体进行绑定了
我们打算未来在启动的时候可以让外部动态的指定端口所以我们可以把端口利用构造暴露出去而ip上面说了不需要绑定固定的ip所以不用管
#pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include unistd.h
#include netinet/in.h
#include arpa/inet.h
#include cstring
#include string#include nocopy.hpp
#include Common.hpp
#include Log.hppusing namespace LogModule;static const int g_socket -1;class UdpServer : public nocopy
{
public:// 构造UdpServer(uint16_t port): _sockfd(g_socket), _port(port){}// 初始化服务器void InitServer(){// 创建 socket 套接字_sockfd ::socket(AF_INET, SOCK_DGRAM, 0); // IPv4 面向数据报if (_sockfd 0){LOG(FATAL, socket create failed\n);exit(SOCKET_ERROR);}LOG(INFO, socket create success, sockfd is %d\n, _sockfd); // 预期 3// 创建存储 服务端主机的 ip 和 端口号信息的 struct sockaddr_in 的结构体struct sockaddr_in local;bzero(local, sizeof(local)); // 清空local.sin_family AF_INET; // 网络通信local.sin_port htons(_port); // 将主机转为网络序列local.sin_addr.s_addr INADDR_ANY; // 与服务器主机不绑定固定的ip而是任意的ip// 将 socket 套接字和 struct sockaddr_in 绑定int n ::bind(_sockfd, (struct sockaddr *)local, sizeof(local));if (n 0){LOG(FATAL, bind failed\n);exit(BIND_ERROR);}LOG(INFO, bind success\n);}// 启动服务器void Start(){}// 析构~UdpServer(){if (_sockfd 0)::close(_sockfd);}private:int _sockfd; // socket 套接字uint16_t _port;// 端口号// std::string _ip;// ip
};
使用 struct sockaddr_in 需要包含头文件
#include netinet/in.h
#include arpa/inet.h AF_INET 表示网络通信当然也可以写成 PF_INET INADDR_ANY 表示绑定任意 IP 地址 bzero 时 cstring 中的一个设置初始值函数和 memset 类似 2.2.3 启动服务
上面我们已经把当前主机某个服务进程的信息ip和port进行了和socket的绑定此时我们就可以启动服务端进行 接收 和 处理 用户的请求了
首先服务端是得先收到客户端的请求然后在处理最后返回给客户端 服务端收到请求的时候也得知道是谁发给你的所以也得用 sockaddr_in 结构体的存储客户端的 ip 和 port 等信息 • 使用 recvfrom 函数进行 接收客户端的请求
处理请求这里的处理就是将收到的信息响应给用户即可
• 使用 sendto 响应给刚刚请求的客户端即可
这里只需要介绍完这里两个函数就可以启动服务了
recvfrom 作用从 sockfd 中接收数据 #include sys/types.h
#include sys/socket.hssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);参数 • sockfd 创建成功的 socket 套接字 • buf 一个接受数据用的缓冲区 • len 缓冲区的大小 • flag 读取方式阻塞/非阻塞 • src_addr 表示客户端请求的 ip 和 port 信息 • addrlen 客户端信息结构体的大小 所以我们在接受客户端的请求前先得有一个 sockaddr_in 的结构体来记录请求客户端的信息
返回值
成功返回收到的字节数失败返回-1 ssize_t 就是 long int sendto 作用通过 sockfd 给指定的 dest_addr 发送数据 #include sys/types.h
#include sys/socket.hssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);参数 • sockfd 创建成功的 socket 套接字 • buf 一个接受数据用的缓冲区 • len 缓冲区的大小 • flag 读取方式阻塞/非阻塞 • dest_addr 表示发送给客户端的 ip 和 port信息 • addrlen 客户端信息结构体的大小 返回值
成功返回发送的字节数失败返回-1 下面就是 启动服务的代码
// 启动服务器
void Start()
{_isrunning true;while (_isrunning){// 创建客户端的 ip 端口信息的结构体struct sockaddr_in peer;socklen_t len sizeof(peer);// 接受客户端的请求char buffer[1024];ssize_t n ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, len);if (n 0){buffer[n] 0;// 处理请求std::string message [echo_server say]# ;message buffer;// 响应给客户端ssize_t m ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)peer, len);if (m 0){LOG(WARNING, sendto failed\n);}}else{LOG(FATAL, recvfrom failed\n);}}_isrunning false;
}
注意在析构的时候需要将 sockfd 给关掉
// 析构
~UdpServer()
{if (_sockfd 0)::close(_sockfd);
}
2.2.4 服务端的全部源码
#pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include unistd.h
#include netinet/in.h
#include arpa/inet.h
#include cstring
#include string#include nocopy.hpp
#include Common.hpp
#include Log.hppusing namespace LogModule;static const int g_socket -1;class UdpServer : public nocopy
{
public:// 构造UdpServer(uint16_t port): _sockfd(g_socket), _port(port), _isrunning(false){}// 初始化服务器void InitServer(){// 创建 socket 套接字_sockfd ::socket(AF_INET, SOCK_DGRAM, 0); // IPv4 面向数据报if (_sockfd 0){LOG(FATAL, socket create failed\n);exit(SOCKET_ERROR);}LOG(INFO, socket create success, sockfd is %d\n, _sockfd); // 预期 3// 创建存储 服务端主机的 ip 和 端口号信息的 struct sockaddr_in 的结构体struct sockaddr_in local;bzero(local, sizeof(local)); // 清空local.sin_family AF_INET; // 网络通信local.sin_port htons(_port); // 将主机转为网络序列local.sin_addr.s_addr INADDR_ANY; // 与服务器主机不绑定固定的ip而是任意的ip// 将 socket 套接字和 struct sockaddr_in 绑定int n ::bind(_sockfd, (struct sockaddr *)local, sizeof(local));if (n 0){LOG(FATAL, bind failed\n);exit(BIND_ERROR);}LOG(INFO, bind success\n);}// 启动服务器void Start(){_isrunning true;while (_isrunning){// 创建客户端的 ip 端口信息的结构体struct sockaddr_in peer;socklen_t len sizeof(peer);// 接受客户端的请求char buffer[1024];ssize_t n ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, len);if (n 0){buffer[n] 0;// 处理请求std::string message [echo_server say]# ;message buffer;// 响应给客户端ssize_t m ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)peer, len);if (m 0){LOG(WARNING, sendto failed\n);}}else{LOG(FATAL, recvfrom failed\n);}}_isrunning false;}// 析构~UdpServer(){if (_sockfd 0)::close(_sockfd);}private:int _sockfd; // socket 套接字uint16_t _port; // 端口号// std::string _ip; // ip 云服务器不需要绑定指定 ipbool _isrunning; // 服务端的状态
};
2.3 客户端设计 客户端的任务分为1、向服务端发送请求 2、接收来自服务端的响应 收发数据都是基于 sockfd以及 ip 、port 的所以我们先把这些字段加上并在构造函数初始化
static const int g_sockfd -1;class UdpClient
{
public:// 构造UdpClient(std::string ip, uint16_t port): _sockfd(g_sockfd), _ip(ip), _port(port){}// 初始化客户端void InitClient(){}// 启动客户端void Start(){}// 析构~UdpClient(){}private:int _sockfd; // socket 套接字uint16_t _port; // 端口号std::string _ip; // ip 地址struct sockaddr_in _server; // 存储服务端的IP、port的结构体
};
2.3.1 初始化客户端
这里我们创建 socket 套接字然后将存储 服务端 的结构体的内容填充即可
// 初始化客户端
void InitClient()
{// 创建套接字_sockfd ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){std::cerr socket error std::endl;exit(1);}// 填充服务器端的数据memset(_server, 0, sizeof(_server)); // 初始值设置为 0_server.sin_family AF_INET; // 网络通信_server.sin_port htons(_port); // 主机转网络_server.sin_addr.s_addr inet_addr(_ip.c_str()); // 将点分十进制的ip转为整数// 注意这里客户端不需要 显示 的绑定自己的 ip 和 端口OS 会自动绑定
}
注意客户端这里我们是不需用户显示要绑定 ip 和 port 的最后解释原因
2.3.2 启动客户端
启动客户端这里的任务主要是向服务器发送数据然后结束反馈显示给用户即可
// 启动客户端
void Start()
{// 长服务while (true){// 客户端需要发送的消息std::string message;std::cout Please Enter ;std::getline(std::cin, message);// 向服务端发送请求int n ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)_server, sizeof(_server));if (n 0) // 发送成功接受响应{// 记录服务端的ip和port等信息struct sockaddr_in peer;socklen_t len sizeof(peer);// 创建接收数据的 缓冲区char buffer[1024];// 接收响应ssize_t m ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, len);if (m 0){buffer[m] 0;std::cout buffer std::endl;// 接收到的数据显示给用户}else{std::cerr recvfrom error std::endl;break;}}else{std::cerr sendto error std::endl;break;}}
}
注意析构的时候需要将sockfd 给关掉
// 析构
~UdpClient()
{if(_sockfd 0)::close(_sockfd);
}
2.3.3 客户端全部源码
#pragma once#include iostream
#include string
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include unistd.h
#include cstringstatic const int g_sockfd -1;class UdpClient
{
public:// 构造UdpClient(std::string ip, uint16_t port): _sockfd(g_sockfd), _ip(ip), _port(port){}// 初始化客户端void InitClient(){// 创建套接字_sockfd ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){std::cerr socket error std::endl;exit(1);}// 填充服务器端的数据memset(_server, 0, sizeof(_server)); // 初始值设置为 0_server.sin_family AF_INET; // 网络通信_server.sin_port htons(_port); // 主机转网络_server.sin_addr.s_addr inet_addr(_ip.c_str()); // 将点分十进制的ip转为整数// 注意这里客户端不需要 显示 的绑定自己的 ip 和 端口OS 会自动绑定}// 启动客户端void Start(){// 长服务while (true){// 客户端需要发送的消息std::string message;std::cout Please Enter ;std::getline(std::cin, message);// 向服务端发送请求int n ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)_server, sizeof(_server));if (n 0) // 发送成功接受响应{// 记录服务端的ip和port等信息struct sockaddr_in peer;socklen_t len sizeof(peer);// 创建接收数据的 缓冲区char buffer[1024];// 接收响应ssize_t m ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, len);if (m 0){buffer[m] 0;std::cout buffer std::endl;// 接收到的数据显示给用户}else{std::cerr recvfrom error std::endl;break;}}else{std::cerr sendto error std::endl;break;}}}// 析构~UdpClient(){if(_sockfd 0)::close(_sockfd);}private:int _sockfd; // socket 套接字uint16_t _port; // 端口号std::string _ip; // ip 地址struct sockaddr_in _server; // 存储服务端的IP、port的结构体
};
主函数修改
前面设计的时候说了我们需要动态的指定服务端的 port 以及客户端的 ipport我们这里采用以前系统部分介绍的命令行参数来实现
#include UdpServer.hpp
#include memory// ./udpserver 8888
int main(int argc, char* argv[])
{if(argc ! 2){std::cerr Usage argv[0] port std::endl;exit(1);}uint16_t port std::stoi(argv[1]);std::unique_ptrUdpServer usvr std::make_uniqueUdpServer(port);// C14usvr-InitServer();usvr-Start();return 0;
}
#include UdpClient.hpp
#include memory
// ./udpclient 127.0.0.1 8888
int main(int argc, char* argv[])
{if(argc ! 3){std::cerr Usage argv[0] ip port std::endl;exit(1);}std::string ip argv[1];uint16_t port std::stoi(argv[2]);std::unique_ptrUdpClient uclt std::make_uniqueUdpClient(ip,port);// C14uclt-InitClient();uclt-Start();return 0;
}
我们先来测试一下效果,再来解决上面的遗留问题 是没有问题的下面这是我云服务器的公网ip这也证明了服务端可以接收不同ip的请求
2.3.4 解决遗留问题
为什么云服务器上服务器端不能绑定指定的ip 云服务器如果绑定了一个特定的ip 就只能接收来自该ip端口的客户端的请求非 该ip的请求都请求不了一般我们采用的是将服务端的 ip 设置为 INADDR_ANY 0.0.0.0他表示任意地址这就意味着服务器接受 任意 ip服务器端口的客户端 访问该服务 举个例子一般服务器会开放一个端口供客户端访问我们把这个端口比作一个小区的警务室警务室有很多的通信设备有线电话、手机、对讲机等这些设备就是ip如果这个警务室指定了有线电话接受外界的通信请求的话外界就只能用电话手机、对讲机等都打不通而如果不指定通信设备的话外界就可以使用任意的同学设备给警务室通信而不指定具体的通信设备就是 IP 中的INADDR_ANY
为什么客户端不需要 显示 的绑定ip和端口 首先注意的是客户端不是不需要绑定ip和端口只是不用用户显示的绑定而是由OS随机选择一个端口进行绑定的原因是端口号资源是有限的而客户端主机的进程不止一个众多进程/服务无法做到客户端的端口号不冲突所以如果客户端显示的绑定就会造成同一主机不同进程的端口冲突这就很不好 例如快手 觉得 8888 这个端口号给他的客户端显示的绑定了隔壁 抖音 觉的8888也很好也给他的客户端绑定了此时它两冲突了造成的后果就是你一旦打开抖音就开不了快手显然我们平时不是这样的
现在的问题是端口号既然是OS随机选择的什么时候选择的如何选择 当客户端第一次向服务端发送请求的时候OS会自动的给客户端选择一个没被用的端口进行和本机ip绑定而这个被随机选择的端口被称为临时端口/客户端端口至于客户端ip也是由OS和网络配置决定的用户不用操作 2.4 优化和验证
我们发现上面的客户端可以请求服务端了服务端也可以将收到的请求处理并返回给客户端了但是不够优雅为我们期望看到的是服务端回显客户端的的ip和port的信息这样我么可以验证上面说的客户端的端口是OS随机选择的
由于服务端拿到的是网络序列所以我们进行小优化一下在服务端拿到主机序列所以我们写一个类专门完成这个事情
#pragma once
#include string
#include netinet/in.h
#include arpa/inet.hclass Int_Addr
{
private:void ToHost(){// 将网络序列转为本机序列_ip inet_ntoa(_addr.sin_addr);_port ntohs(_addr.sin_port);}public:Int_Addr(const struct sockaddr_in peer): _addr(peer){ToHost();}std::string Ip(){return _ip;}uint16_t Port(){return _port;}~Int_Addr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};
此时我们把他给加到服务端 现在来看一下效果 此时服务端就可以看到来自客户端请求的ip和端口了而且我们客户端是没有指定端口的这里他们的端口是不一样的 OK因为这里第一个UDP网络程序很多东西的细节以前没有介绍过所以这里就很详细以后我们不在介绍这么详细了就直接用了 请点击我的gitee链接查看全部源码echo_server全部源码 3、简单的英译汉
这个网络程序打算实现的功能是客户端输入一个单词服务端进行翻译成汉语 仔细一分析要实现这个样的程序好像不难直接在上面的 echo_server 的基础上加业务即可这个业务就是实现将一个单词翻译成汉语
为了降低耦合度我们可以将执行翻译的这部分单独封装成一个类利用包装器包装成一个可调用对象给服务端这个可调用对象服务端只需要调用即可这样服务端并不关心处理业务在干啥只需要调指定的可调用函数即可做到了完美的解耦优雅~
3.1 实现Dict 类 我们将采用文件级别的英汉映射所以提前得准备一个文件外面在初始化对象的时候只需要把这个文件的路径传过来就可以然后构造的时候去加载该文件中的单词和汉语将他们放到哈希表中最后向外部提供翻译的接口 Transform 即可 #pragma once#include iostream
#include string
#include unordered_map#include Log.hpp
#include Common.hppusing namespace LogModule;const std::string sep :;class Dict
{
private:void Load(){// 打开文件std::ifstream in(_path);if (!in.is_open()){LOG(FATAL, file: %s open failed\n, _path);exit(FILE_OPEN);}// 读取每一行std::string line;while (std::getline(in, line)){LOG(DEBUG, load %s success\n, line.c_str());if (line.empty()) // 空行continue;auto pos line.find(sep);if (pos std::string::npos) // 最后只有分隔符continue;std::string key line.substr(0, pos);if (key.empty()) // xxxxcontinue;std::string value line.substr(pos sep.size());if (value.empty()) // xxxxx:continue;_dict.insert(std::make_pair(key, value));}LOG(DEBUG, load done\n);in.close();}public:Dict(const std::string path): _path(path){Load();}std::string Transform(std::string word){if (word.empty())return None;auto iter _dict.find(word);return iter _dict.end() ? None : iter-second;}~Dict(){}private:std::string _path; // 文件的路径std::unordered_mapstd::string, std::string _dict; // 映射
};
因为这个逻辑不复杂所以直接就给源码了
3.2 完成服务端的修改
其实服务端和上面的 echo_server 几乎是一样的不同的是加了一个可调用对象的属性 包装一个参数string 返回值 string 的 func_t 的函数对象
这个对象是外部启动服务器的时候传过来的服务器只是调用它 仅仅是和上面的 echo_server 就多了一个 调用 _func 此时服务器也不知道 _func 在干嘛服务器只负责收发数据
3.3 主函数完善
#include UdpServer.hpp
#include Dict.hpp
#include memoryint main(int argc, char* argv[])
{if(argc ! 2){std::cerr Usage argv[0] port std::endl;exit(1);}uint16_t port std::stoi(argv[1]);Dict dict(./dict.txt);func_t transfrom std::bind(Dict::Transform, dict, std::placeholders::_1);std::unique_ptrUdpServer usvr std::make_uniqueUdpServer(port, transfrom);// C14usvr-InitServer();usvr-Start();return 0;
}
客户端只是创建了一个 Dict 对象然后将它里面的 Transform 函数进行了绑定然后给了服务器对象关于std::bind 这里就是将 Dict 类中的Transform函数和dict对象强关联了此时就是返回值 string 参数string 的 可调用对象了std::bind是解耦的神器后面经常看到~
注意这里客户端是不用动的 请点击我的gitee链接查看全部源码dict 全部源码 4、多人聊天室
最后一个UDP套接字的网络程序我们打算实现一个简单的在线多人聊天室
主要功能是不同主机的客户端都可以向服务端发送消息服务端在将这些消息转给所有在线的客户端即实现了多人聊天
其实这里和上面的翻译程序很相似但是我们这里要采用多线程的转发什么意思呢
我们服务端的主线程负责收消息向每个在线的用户转发的事情将采用线程转发
由于转发是给每一个在线的用户转发的所以我们需要在维护一个类Route专门负责维护在线用户和转发的它里面提供一个转发的函数外部主函数进行包装主线程直接执行这个可调用的函数对象即可即降低了耦合度
在Route的转发函数内部将采用线程池当主线程调用时只需要将转发的任务包装成线程池的任务类型放到线程池即可之后线程池中的线程就可以调用可这里的线程池我们也采用我们自己以前手撕的那个~ 4.1 服务端改造
我们这个程序还是基于 echo_server 的所以直接在它上面改造即可
UdpServer.hpp 服务端 只需要把之前的转发消息换成_func即可 _func 是主函数启动服务器时给的上面包装的类型是 server_t 是因为func_t 和线程池中的冲突了这里因为要转发消息所以得把 sockfd、转发的消息都给过去最后给 Int_Addr 的addr是因为转发时得判断当前的用户是否在用户列表中
而服务端主函数给的可调用对象是包装的Route中的Forward的所我们先介绍Route类
4.1.1 Route类实现
Route类的主要作用是向所有的在线用户转发消介绍到的消息 所以得维护一个在线的用户列表可以使用 vector类型就是 Int_Addr主线程进来先检查该用户是否存在然后判断是不是要退出消息是Q或者QUIT代表退出如果是直接将从vector中删除即可如果不删除就包装线程池中线程的回调的函数然后获取线程池的句柄我们以前实现的是单例的将任务放到线程池即可 #pragma once#include iostream
#include vector
#include functional
#include netinet/in.h
#include arpa/inet.h
#include sys/types.h
#include sys/socket.h
#include pthread.h#include Inet_Addr.hpp
#include Log.hppclass Route
{
private:void Check(Int_Addr who){}void Offline(Int_Addr who){}// 线程池中的线程回调的任务函数void ForwardHelper(int sockfd, std::string message, Int_Addr who){}public:Route(){}void Forward(int sockfd, std::string message, Int_Addr who){// 1、检查用户是否在在线列表 ---》a.不在添加 b.在啥都不做Check(who);// 2、用户需要离线离线if (message QUIT || message Q){Offline(who);}// 3、向你在线的用户转发消息// 让线程去转发// 包装函数// 获取句柄推送任务}~Route(){}private:std::vectorInt_Addr _online_users; // 在线用户列表
};
这就是大概的框架先来实现一下上面的检查用户在线、是否下线、线程池中的回调转发消息的函数
Check
void Check(Int_Addr who)
{for (auto user : _online_users){if (user who)return; // 用户已经存在}// 不存在添加_online_users.emplace_back(who);LOG(DEBUG, %s add success\n, who.AddrStr().c_str());
}
这里使用到了 而Int_Addr中没有实现所以得加一下 Offline
void Offline(Int_Addr who)
{auto iter _online_users.begin();for (; iter ! _online_users.end(); iter){if (*iter who){LOG(DEBUG, %s remove success\n, who.AddrStr().c_str());_online_users.erase(iter);break; // 避免迭代器失效}}
} ForwardHelper
void ForwardHelper(int sockfd, std::string message, Int_Addr who)
{std::string send_message [ who.AddrStr() ]# message;// 转发for (auto user : _online_users){struct sockaddr_in peer user.Addr(); // 发给谁::sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)peer, sizeof(peer));LOG(INFO, %s forward a message is %s\n, who.AddrStr(), send_message.c_str());}
}
OK接下来就是包装 线程的可调用对象了转发需要的就是 sockfd 和 message 、who也就是和上面的 ForWardHelper 绑死后面线程直接调用即可无需参数
void Forward(int sockfd, std::string message, Int_Addr who)
{// 1、检查用户是否在在线列表 ---》a.不在添加 b.在啥都不做Check(who);// 2、用户需要离线离线if (message QUIT || message Q){Offline(who);}// 3、向你在线的用户转发消息// 让线程去转发task_t t std::bind(Route::ForwardHelper, this, sockfd, message, who);ThreadPooltask_t::getInstance()-PushTask(t);
}
OK到这里我们的Route的类基本上就设计好了但是还差最后一口气这里的检查和下线是主线程做的向所有的在线用户转发时新线程做的而他们的操作都会操作同一个vector可能造成线程安全的问题所以得保护如何保护加锁
#pragma once#include iostream
#include vector
#include functional
#include netinet/in.h
#include arpa/inet.h
#include sys/types.h
#include sys/socket.h
#include pthread.h#include Inet_Addr.hpp
#include Log.hpp
#include ThreadPool.hpp
#include LockGuard.hppusing namespace LogModule;using task_t std::functionvoid(void); // 包装一个线程的任务函数class Route
{
private:void Check(Int_Addr who){LockGuard lockguard(_mutex);for (auto user : _online_users){if (user who)return; // 用户已经存在}// 不存在添加_online_users.emplace_back(who);LOG(DEBUG, %s add success\n, who.AddrStr().c_str());}void Offline(Int_Addr who){LockGuard lockguard(_mutex);auto iter _online_users.begin();for (; iter ! _online_users.end(); iter){if (*iter who){LOG(DEBUG, %s remove success\n, who.AddrStr().c_str());_online_users.erase(iter);break; // 避免迭代器失效}}}void ForwardHelper(int sockfd, std::string message, Int_Addr who){LockGuard lockguard(_mutex);std::string send_message [ who.AddrStr() ]# message;// 转发for (auto user : _online_users){struct sockaddr_in peer user.Addr(); // 发给谁::sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)peer, sizeof(peer));LOG(INFO, %s forward a message is %s\n, who.AddrStr(), send_message.c_str());}}public:Route(){pthread_mutex_init(_mutex, nullptr);}void Forward(int sockfd, std::string message, Int_Addr who){// 1、检查用户是否在在线列表 ---》a.不在添加 b.在啥都不做Check(who);// 2、用户需要离线离线if (message QUIT || message Q){Offline(who);}// 3、向你在线的用户转发消息// 让线程去转发task_t t std::bind(Route::ForwardHelper, this, sockfd, message, who);ThreadPooltask_t::getInstance()-PushTask(t);}~Route(){pthread_mutex_destroy(_mutex);}private:std::vectorInt_Addr _online_users; // 在线用户列表pthread_mutex_t _mutex; // 互斥锁
};
这样就好了最后还差服务端的主函数的传递给服务端可调用对象了
4.1.2 服务端主函数修改
只需要创建 Route 对象然后绑定一个可接受三个参数的函数对象即可
#include UdpServer.hpp
#include Route.hpp
#include memoryint main(int argc, char* argv[])
{if(argc ! 2){std::cerr Usage argv[0] port std::endl;exit(1);}uint16_t port std::stoi(argv[1]);Route route;server_t forward std::bind(Route::Forward, route, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);std::unique_ptrUdpServer usvr std::make_uniqueUdpServer(port, forward);// C14usvr-InitServer();usvr-Start();return 0;
}
4.2 客户端改造
我们期望客户端使用两个不用的线程分别进行收发消息所以我们需要将原来的客户端的 Start函数进行改造成两个函数供两个线程分别调用
// 收消息
void RecvMsg(const std::string name)
{while (true){// 记录服务端的ip和port等信息struct sockaddr_in peer;socklen_t len sizeof(peer);// 创建接收数据的 缓冲区char buffer[1024];// 接收响应ssize_t m ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, len);if (m 0){buffer[m] 0;std::cerr buffer std::endl; // 接收到的数据显示给用户}else{std::cerr recvfrom error std::endl;break;}}
}void SendMsg(const std::string name)
{// 长服务std::string cli_profix name # ;while (true){// 客户端需要发送的消息std::string message;std::cout cli_profix;std::getline(std::cin, message);// 向服务端发送请求int n ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)_server, sizeof(_server));if (n 0){std::cerr sendto error std::endl;break;}}
}
4.2.1 客户端的主函数改造
客户端的主函数就需要创建两个线程来分别执行收消息和发消息的任务
#include UdpClient.hpp
#include Thread.hpp
#include memoryusing namespace ThreadModule;int main(int argc, char *argv[])
{if (argc ! 3){std::cerr Usage argv[0] ip port std::endl;exit(1);}std::string ip argv[1];uint16_t port std::stoi(argv[2]);std::unique_ptrUdpClient uclt std::make_uniqueUdpClient(ip, port); // C14// 创建两个线程分别负责 收发消息Thread sender(sender-thread, std::bind(UdpClient::SendMsg, uclt.get(), std::placeholders::_1));Thread recver(recver-thread, std::bind(UdpClient::RecvMsg, uclt.get(), std::placeholders::_1));sender.start();recver.start();sender.join();recver.join();return 0;
}
注意这里bind时因为这里uclt是智能指针对象所以需要获取原生对象的指针就需要使用get方法这点我们在【智能指针】介绍过
OK测试一下 这里我是在一台主机上使用 本地环回 和 公网ip 进行模拟的为了让页面看起来不那么混乱我用了 管道 和 重定向 分离因为客户端收到的消息是使用 std::cerr 打印的也就是文件描述符是2可以利用重定向将他们分开将输出的cerr的内容都放到管道一个专门的终端读取即可每个客户端都有一个管道和读取管道的终端正如你的微信有一个输入区域就是这里的下面的那个终端有显示群消息的上面的那个终端且每个微信都是有各自的这两个的
请点击我的gitee链接查看全部源码chat全部源码 5、地址函数补充
sockaddr_in 中的成员 sin_addr.s_addr 表示一个32位的整数的 ip 地址但是我们通常使用点分十进制的字符串表示 ip 地址以下函数可以在 字符串 和 整数的ip 之间进行转换
5.1 字符串转整数
// 将 cp 字符串转为 整数
int inet_aton(const char *cp, struct in_addr *inp);// typedef uint32_t in_addr_t;
in_addr_t inet_addr(const char *cp);// cp 字符串转为 整数 返回// af : IpV4和IPv6的哪一个 src : 表示字符串ip dst :被转换之后的整数struct sockaddr_in 的 sin_addr.s_addr
int inet_pton(int af, const char *src, void *dst);
5.2 整数转字符串
// 将整数转为 整数 ip 返回
char *inet_ntoa(struct in_addr in);// af : IpV4和IPv6的哪一个 src代表整数ip dst用户指定的字符串缓冲区 size 缓冲区大小
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);其中 inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6 的 in6_addr,因此函数接口是 void *
#include iostream
#include netinet/in.h
#include arpa/inet.hint main()
{ struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr 0;addr2.sin_addr.s_addr 0xffffffff;char* ptr1 inet_ntoa(addr1.sin_addr);char* ptr2 inet_ntoa(addr2.sin_addr);printf(ptr1: %s, ptr2: %s\n, ptr1, ptr2);return 0;
} 我们发现第二次把第一次的给覆盖了 原因是 inet_ntoa 把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆 盖掉上一次的结果 这里就会有问题如果有多个线程调用 inet_ntoa 可能会出现异常的情况
在 APUE 中, 明确提出 inet_ntoa 不是线程安全的函数
但是在 centos7 上测试了没有出问题内部可能加了互斥锁猜测 在多线程环境下, 推荐使用 inet_ntop, 这个函数由调用者提供一个缓冲区保存 结果, 可以规避线程安全 由于 centos7 已停止维护所以我们写一段代码在 ubuntu 上验证一下 #include stdio.h
#include unistd.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include pthread.h
void *Func1(void *p)
{struct sockaddr_in *addr (struct sockaddr_in *)p;while (1){char *ptr inet_ntoa(addr-sin_addr);printf(addr1: %s\n, ptr);}return NULL;
}
void *Func2(void *p)
{struct sockaddr_in *addr (struct sockaddr_in *)p;while (1){char *ptr inet_ntoa(addr-sin_addr);printf(addr2: %s\n, ptr);}return NULL;
}
int main()
{pthread_t tid1 0;struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr 0;addr2.sin_addr.s_addr 0xffffffff;pthread_create(tid1, NULL, Func1, addr1);pthread_t tid2 0;pthread_create(tid2, NULL, Func2, addr2);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
} 我大概测试了5次都没有出错 但是我们还是建议在多线程下使用 inet_ntop 和 inet_pton OK好兄弟本期分享就到这里我是 cp 我们下期再见~