【网络】套接字编程——UDP通信

> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标: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

三、结束语 

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

​​ 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/58143.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

黑马官网2024最新前端就业课V8.5笔记---HTML篇

Html 定义 HTML 超文本标记语言——HyperText Markup Language。 标签语法 标签成对出现&#xff0c;中间包裹内容<>里面放英文字母&#xff08;标签名&#xff09;结束标签比开始标签多 /拓展 &#xff1a; 双标签&#xff1a;成对出现的标签 单标签&#xff1a;只有开…

openfoam中生成的3d案例提取得到slice后的2d案例

问题&#xff1a; 由于前期准备做3d的案例&#xff0c;并且模拟也比较费时间&#xff0c;现在生成了几十份3d的数据&#xff0c;但是现在只想要2d的数据来演示&#xff0c;该如何提取或者转换呢&#xff1f; 解决方法&#xff1a; 1.说明图片中的每个2d视图的points都是恒定不…

使用 Sortable.js 库 实现 Vue3 elementPlus 的 el-table 拖拽排序

文章目录 实现效果Sortable.js介绍下载依赖添加类名导入sortablejs初始化拖拽实例拖拽完成后的处理总结 在开发过程中&#xff0c;我们经常需要处理表格数据&#xff0c;并为用户提供便捷的排序方式。特别是在需要管理长列表、分类数据或动态内容时&#xff0c;拖拽排序功能显得…

STM32 + CubeMX + 硬件SPI + W5500 +UDP

这篇文章记录一下STM32W5500UDP的调试过程&#xff0c;实现UDP数据的接收与发送。 目录 一、W5500模块介绍二、Stm32CubeMx配置三、Keil代码编写1、添加W5500驱动代码到工程&#xff08;添加方法不赘述&#xff0c;驱动代码可以在官网找&#xff09;2、在工程中增加代码&#…

2023年SCRM系统排名分析及市场趋势解读

内容概要 当前&#xff0c;SCRM&#xff08;社交客户关系管理&#xff09;系统在企业运营中的重要性日益凸显&#xff0c;尤其是在快速发展的数字经济环境中。2023年的SCRM市场展现出多元化与专业化的趋势&#xff0c;不同企业在客户关系管理方面的需求各有不同&#xff0c;这…

StableDiffusion-3.5 文生图模型本地部署尝鲜

文章目录 官方仓库ComfyUI 配置模型文件生成图片&#xff0c;观察日志生成样例 买了新的 4070TiS 显卡之后&#xff0c;终于有了个人的 16GB 显存&#xff0c;再也不用在关键时刻和实验室的其他人抢那两张 3080Ti 12G 了&#xff0c;所以想试试看干净的 Linux 环境下&#xff0…

规范:项目、目录、文件、样式、事件、变量、方法、url参数、注释、git提交 命名规范及考证

一、规范命名的重要性 易懂、通用、规范、标准、专业性、是经验积累的体现 1.1、常见命名方法 序号命名方法解释1全小写2全大写3驼峰&#xff1a;小驼峰命名法4驼峰&#xff1a;大驼峰命名法5烤串命名法 / 脊柱命名法6下划线分隔法 二、项目名 采用小写字母和中划线&#…

Navicat 连接远程腾讯云服务器的MySQL数据库

首先需要开放开放腾讯云安全端口&#xff0c;可以参考这个链接腾讯云服务器入站规则端口开放使用指南(CentOS系统)。 但是注意需要开放的是IPv6&#xff0c;这个可以通过netstat命令查看确认。 然后查看当前用户信息 select user, host from mysql.user一般看到的都是 localh…

第三十四篇:URL和URI的区别,HTTP系列一

前面我们讲到通过TCP协议通信双方建立可靠连接&#xff0c;那么此时双方进行通信&#xff0c;需要用人能理解的形式进行信息组织&#xff0c;也就是为各种特定需求服务&#xff0c;满足日常生活中的各种场景。 比如&#xff1a;网页浏览、电子邮件、远程登录、文件传输、网络管…

什么情况下,不推荐建立索引?

一般有以下几种情况不推荐建立索引&#xff1a; 1&#xff09;对于数据量很小的表 当表的数据量很小&#xff08;如几百条记录&#xff09;时&#xff0c;建立索引并不会显著提高查询性能&#xff0c;反而可能增加管理的复杂性&#xff1b; 2&#xff09;频繁更新的表 对于…

GitHub上传自己的项目

目录 一、安装Git插件 1&#xff09;下载 2&#xff09;安装 二、创建Gothub的创库 三、通过Git上传本地文件到Github 四、其他 1、部分指令 2、如果已经运行过git init并设置了[user]&#xff0c;下次可以直接用 一、安装Git插件 1&#xff09;下载 下载地址&#x…

「Mac畅玩鸿蒙与硬件26」UI互动应用篇3 - 倒计时和提醒功能实现

本篇将带领你实现一个倒计时和提醒功能的应用&#xff0c;用户可以设置倒计时时间并开始计时。当倒计时结束时&#xff0c;应用会显示提醒。该项目涉及时间控制、状态管理和用户交互&#xff0c;是学习鸿蒙应用开发的绝佳实践项目。 关键词 UI互动应用倒计时器状态管理用户交互…

Linux动态库和静态库

1&#xff0c;手动制作静态库 1&#xff0c;如何形成静态库文件 做库时&#xff0c;头文件(.h)必须暴露&#xff0c;源文件(.c)必须隐藏。 操作&#xff1a;将需要形成库的文件编译成.o文件&#xff1a; 然后用指令&#xff1a;ar -rc libmy_stdio.a my_stdio.o my_string.o…

java基础之 String\StringBuffer\ StringBuilder

文章目录 String字符串的创建为什么说String是不可变的&#xff1f;创建后的字符串存储在哪里&#xff1f;字符串的拼接String类的常用方法 StringBuilder & StringBuffer使用方法验证StringBuffer和StringBuilder的线程安全问题 总结三者区别什么情况下用运算符进行字符串…

告别繁琐统计,一键掌握微信数据

微信数据管理的挑战在数字时代&#xff0c;微信已成为我们日常沟通和商业活动的重要工具。然而&#xff0c;随着微信号数量的增加&#xff0c;手动统计每个账号的数据变得越来越繁琐。从好友数量到会话记录&#xff0c;再到转账和红包&#xff0c;每一项都需要耗费大量的时间和…

bert-base-chinese模型使用教程

向量编码和向量相似度展示 import torch from transformers import BertTokenizer, BertModel import numpy as npmodel_name "C:/Users/Administrator.DESKTOP-TPJL4TC/.cache/modelscope/hub/tiansz/bert-base-chinese"sentences [春眠不觉晓, 大梦谁先觉, 浓睡…

HTML+CSS科技感时钟(附源码!!!)

预览效果 源码(直接复制使用) <!DOCTYPE html> <html lang"zh-Hans"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>科技感时钟</…

PyQt5实战——UTF-8编码器功能的实现(六)

个人博客&#xff1a;苏三有春的博客 系类往期文章&#xff1a; PyQt5实战——多脚本集合包&#xff0c;前言与环境配置&#xff08;一&#xff09; PyQt5实战——多脚本集合包&#xff0c;UI以及工程布局&#xff08;二&#xff09; PyQt5实战——多脚本集合包&#xff0c;程序…

‌MySQL中‌between and的基本用法‌

文章目录 一、between and语法二、使用示例2.1、between and数值查询2.2、between and时间范围查询2.3、not between and示例 BETWEEN AND操作符可以用于数值、日期等类型的字段&#xff0c;包括边界值。 一、between and语法 MySQL中的BETWEEN AND操作符用于在两个值之间选择…

微服务系列一:基础拆分实践

目录 前言 一、认识微服务 1.1 单体架构 VS 微服务架构 1.2 微服务的集大成者&#xff1a;SpringCloud 1.3 微服务拆分原则 1.4 微服务拆分方式 二、微服务拆分入门步骤 &#xff1a;以拆分商品模块为例 三、服务注册订阅与远程调用&#xff1a;以拆分购物车为例 3.1 …