当前位置: 首页 > news >正文

【socket编程】UDP网络通信 {简单的服务器echo程序;简单的远程控制程序;简单的网络聊天室程序}

今天我们通过以下的几个surver/client模型了解一下UDP网络通信

一、简单的服务器echo程序

以下部分内容转载自「网络编程」简单UDP网络通信程序的实现_socket udp-CSDN博客

1.1 服务端

首先明确,这个简单的UDP网络程序分客户端和服务端,所以我们要生成两个可执行程序,一个是客户端的,另一个是服务端的,服务端充当的是服务器,暂时实现的功能是客户端和服务端简单进行通信,服务端要可以收到客户端发送给服务端的信息,目前就先简单实现这样的功能

下面进行编写服务端的代码

1.1.1 创建套接字文件

先介绍创建套接字文件的函数socket

socket函数

socket函数的作用是创建套接字文件,TCP/UDP 均可使用该函数进行创建套接字,man 2 socket查看:

img

create an endpoint for communication:创建通信端点,即创建通信的一端

函数:socket头文件:#include <sys/types.h>#include <sys/socket.h>函数原型:int socket(int domain, int type, int protocol);参数:第一个参数domain:套接字类型第二个参数type:数据的传输方式第三参数protocol:创建套接字的协议类别返回值:套接字创建成功返回一个文件描述符,创建失败返回-1,错误码被设置
  • socket系统调用接口是对传输层的文件系统级别的封装,Linux下一切皆文件!
  • socket函数用于创建套接字文件描述符
  • 也就是说后续在进行网络读写时,可以用文件接口进行字节流读写
  • TCP协议的特点是面向字节流传输,所以可以使用文件接口进行网络读写
  • UDP协议的特点是面向数据报传输,所以不适用文件接口的字节流读写,UPD有自己专属的读写接口:recvfromsendto

socket函数的参数

(1)socket函数的第一个参数是domain,用于创建套接字的类型,该参数就相当于 struct sockaddr结构体的前16位,即2字节

img

该domain参数的选项已经设置好了,我们直接选用即可。该参数的选项很多,我们常用的也就几个:

  • 如果要选择本地通信,则选择 AF_UNIX
  • 如果要选择网络通信,则选择 AF_INET(IPv4)或者 AF_INET6(IPv6)

“inet” 是Internet Protocol(IP)的简写

img

(2)socket函数的第二个参数是type,用于创建套接字时提供的数据传输方式

该参数的选项也是已经设置好了,我们直接选用即可。该参数的选项很多,我们常用的也就几个:

  • 如果是基于UDP的网络通信,我们采用的就是 SOCK_DGRAM,套接字数据报,提供的用户数据报服务(对应UDP的特点:面向数据报)
  • 如果是基于TCP的网络通信,我们采用的就是 SOCK_STREAM,流式套接字,提供的是流式服务(对应TCP的特点:面向字节流)

SOCK_DGRAM对应的英文:socket datagram
SOCK_STREAM对应的英文:socket stream

img

(3)socket函数的第三个参数是protocol,用于创建套接字的协议类别。

可以指明为TCP或UDP,但该字段一般直接设置为0就可以了。
设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议

socket函数的返回值

套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码被设置

img

解释套接字创建成功返回一个文件描述符的问题

  • 当我们调用socket函数创建套接字时,实际相当于我们打开了一个“网络文件”,而这个网络文件得底层硬件实际就是“网卡”
  • 文件描述符下标0、1、2依次被标准输入、标准输出以及标准错误占用,
  • 如果程序没有打开其他文件,当套接字创建成功时,文件描述符下标为3的指针就指向了这个打开的 “网络文件”
  • 我们读取、发送数据,就从这个 “网络文件” 进行读取和发送
  • 所以操作网络就像操作文件一般,这个“网络文件”就是一个缓冲区

明确一点

  • 按照TCP/IP四层模型来说,自顶向下依次是应用层、传输层、网络层和数据链路层。
  • 而我们现在所写的代码都叫做用户级代码,也就是说我们是在应用层编写代码
  • 因此我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,也就意味着我们在应用层调用的接口都叫做系统调用接口

img


1.1.2 填充套接字结构+绑定端口

绑定端口的函数是bind函数

bind函数

bind函数的作用是绑定端口号,TCP/UDP 均可使用进行该函数绑定端口,man 2 bind查看:

img

bind a name to a socket:将名称绑定到套接字

函数:bind头文件:#include <sys/types.h>#include <sys/socket.h>函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数:第一个参数sockfd:文件描述符第二个参数addr:套接字结构的地址第三参数addrlen:套接字结构的大小返回值:绑定成功返回0,绑定失败返回-1,同时错误码会被设置

下面介绍bind函数的参数

(1)bind函数的第一个参数是sockfd,是套接字文件的文件描述符

(2)bind函数的第二个参数是addr,是套接字结构, 用于保存网络相关的属性信息,比如IP地址、端口号等

该参数addr的类型是:struct sockaddr *,也就是如图的结构体:

img

我们要做的工作就是:定义一个 sockaddr_in 的结构体,也就是上图的第二个结构体,然后对该结构体进行内容填充,填完就把给结构体传给第二个参数addr,需要强制类型转换

套接字结构 sockaddr_in

我们看一下 sockaddr_in 结构体的定义:

img

可以看到,sockaddr_in 有以下几个成员类型:

  • sin_family:表示协议家族,类型uint16_t。实际就是套接字类型(AF_INET, AF_UNIX等)

  • sin_port:表示端口号,类型uint16_t。(填充时需要转网络序列)

  • sin_addr.s_addr:表示IP地址,类型uint32_t。(填充时需要将字符串转4字节整形,并转为网络序列)

  • 剩下的字段不关注

端口号不能随意绑定

需要注意的是,不是所有的端口号都能成功绑定:如0~1023号端口被系统保留用于一些特定的服务和应用程序(系统端口),不允许绑定。还有一些熟知端口同样也不要进行绑定。

不建议服务器绑定特定的IP地址

首先,云服务器是不支持的绑定公网IP的;如果使用虚拟机或者独立Linux系统,那么IP地址是支持绑定的。

实际上,一款网络服务器,不建议指明绑定一个IP,上面的服务端指定绑定一个IP是错误的用法

比如你运行服务端的机器上有多个网卡,意味着你的服务端上有多个IP, 一台服务器上端口号为8080的服务只有一个。这台服务器在接收数据时,底层的多张网卡(多个IP)都有可能接收到数据,而这些数据也都是要向上递送给8080服务的。此时如果服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据,而来自其他IP的数据是接收不到的。

推荐服务器绑定的IP是:INADDR_ANY,这是一个宏,代表 0.0.0.0,叫做任意地址绑定。绑定了该IP,只要是发送给端口号为8080的服务的数据,不管来自主机上的哪张网卡,哪个IP系统都会可以将数据自底向上全部递送给服务端。

img

网络序列与主机序列之间的转换

#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);

字符串IP和整型IP之间的转换

头文件:#include<sys/types.h>#include <arpa/inet.h>#include <netinet/in.h>    
函数原型:// 点分十进制字符串 --> 网络整型// 分两步:1.字符串转整型 2.主机转网络in_addr_t inet_addr(const char *cp);  //最简单int inet_aton(const char* cp,  struct in_addr* inp);  //注意第二个参数传in_addr结构的地址即&sin_addrint inet_pton(int af, const char* src, void* dst);  //第一个参数是协议家族,后两个参数和inet_aton相同// 网络整形 --> 点分十进制字符串// 分两步:1.网络转主机 2.in_addr结构转字符串char *inet_ntoa(struct in_addr in);  //函数内部将转换后的字符串保存在静态存储区,因此该函数是不可重入函数,存在线程安全问题。const char* inet_ntop(int ar, const void* src, char* dst, socklen_t size); //多线程环境下推荐使用inet_ntop,该函数需由调用者提供缓冲区,可以规避线程安全问题。参数:1.协议家族 2.整型IP地址 3.字符串缓冲区地址 4.缓冲区大小

1.1.3 从客户端接收数据

服务端要接收客户端发送的消息,接收信息的函数是recvfrom

recvfrom函数的作用是接收信息

img

receive a message from a socket:从套接字接收消息

函数: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);参数:第一个参数sockfd:文件描述符,从哪个套接字文件去读数据第二个参数buf:代表读上来的数据放到哪个缓冲区里面第三参数len:缓冲区的长度第四个参数flags:读取方式,0代表阻塞式读取第五个参数src_addr:数据发送方的套接字结构,从哪读第六个参数addrlen:src_addr结构体的长度返回值:成功返回接收到的字节数,失败返回-1,同时错误码会被设置。对等方执行有序关闭后,返回值将为0

socklen_t 是一个32位的无符号整数

img

第五个参数src_addr:输出型参数,数据发送方的套接字结构地址,recvfrom接收到数据后将对端的套接字结构存入其中。(从哪读)

第六个参数addrlen:输入输出型参数,需要传入src_addr结构体的长度,recvfrom接收到数据后将读取到的套接字结构的大小存入其中。

我们需要定义一个套接字结构struct sockaddr并置为空,将地址强转后传给src_addr,还需要定义一个结构长度socklen_t,并初始化为套接字结构的大小,将其地址传给addrlen。如果不关注数据的来源,后两个参数可以设置为nullptr


1.1.4 服务端代码

udpServer.hpp

#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;// 错误类型枚举
enum
{UAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};const static string defaultIp = "0.0.0.0";
const static int gnum = 1024;class udpServer
{
public:udpServer(const uint16_t &port, const string &ip = defaultIp): _port(port), _ip(ip){}// 初始化服务器void initServer(){// 1.创建套接字文件_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd == -1){cerr << "socket error: " << errno << " : " << strerror(errno) << endl;exit(SOCKET_ERR);}cout << "socket success: " << _sockfd << endl;// 2.绑定端口// 2.1 填充套接字结构sockaddr_instruct sockaddr_in local;bzero(&local, sizeof(local));  // 把 sockaddr_in结构体全部初始化为0local.sin_family = AF_INET;    // 未来通信采用的是网络通信local.sin_port = htons(_port); // htons(_port)主机字节序转网络字节序// 绑定IP方法1:INADDR_ANY// local.sin_addr.s_addr = INADDR_ANY;//服务器的真实写法// 绑定IP方法2:把外部的构造函数传参去掉,使用我们自己定义的string defaultIp = "0.0.0.0";local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)// 2.2 绑定int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&localif (n == -1){cerr << "bind error: " << errno << " : " << strerror(errno) << endl;exit(BIND_ERR);}// UDP server 预备工作完成}// 启动服务器void start(){// 服务器的本质就是一个死循环char buffer[gnum];for (;;){struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);if (s > 0) // 接收成功{buffer[s] = 0;// 发消息对方的IPstring clientip = inet_ntoa(peer.sin_addr); // 直接传sin_addr结构体,整数IP 转 字符串IP(点分十进制IP)// 发消息对方的端口号uint16_t clientport = ntohs(peer.sin_port); // ntohs:网络字节序转主机字节序// 发送的消息string message = buffer;// 打印cout << clientip << "[" << clientport << "]" << "# " << message << endl;}}}~udpServer(){}private:uint16_t _port; // 端口号string _ip;     // ip地址int _sockfd;    // 文件描述符
};

udpServer.cc

#include "udpServer.hpp"
#include <memory>// 使用手册
// ./udpServer port
static void Uage(string proc)
{cout << "\nUage:\n\t" << proc << " local_port\n\n";
}int main(int argc, char *argv[])
{if (argc != 2){Uage(argv[0]);exit(UAGE_ERR);}uint16_t port = atoi(argv[1]); // string to int//不需要传IP了std::unique_ptr<udpServer> usvr(new udpServer(port));usvr->initServer(); // 初始化服务器usvr->start();      // 启动服务器return 0;
}

1.1.5 几个网络相关的命令行工具

ifconfig 命令

ifconfig:interface configuration接口配置,显示和配置网络接口的信息

第一个IP:inet 10.0.4.14,这个IP是内网IP
第二个IP: inet 127.0.0.1,这个IP是本地环回,用于本地测试
注:“inet” 是Internet Protocol(IP)的简写

img

什么是本地环回??

img

  • 所谓本地环回是指client和server发送数据只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中
  • 通常用于本地网络服务器测试,通过本地环回测试的程序后期仍无法通信大概率是网络问题而非编码问题。
  • 本地环回地址通常是127.0.0.1

netstat 命令

netstat是一个用于显示网络连接、路由表和网络接口信息的命令行工具

netstat:network statistics网络统计

常用选项:

  • -a:all (显示所有连接和监听端口)
  • -t:tcp (仅显示TCP连接)
  • -u:udp (仅显示UDP连接)
  • -n:numeric (以数字形式显示IP地址和端口号)
  • -p:program (显示与连接关联的进程信息)
  • -l:listen(显示所有的监听端口)
  • -r:route (显示路由表信息)
  • -s:statistics (显示网络统计信息)

netstat -nuap 查看本机所有的udp连接

Foreign Address:(外部地址)是指与本地计算机建立网络连接的远程计算机的IP地址和端口号,也就是客户端连服务器

0.0.0.0:* 表示任意IP地址、任意的端口号的程序都可以访问当前进程

img


1.2 客户端

1.2.1 关于客户端的绑定问题

明确一点

  • 客户端是需要服务端的IP和端口号的,没有这些客户端就连不上服务端
  • 也就是说服务端的 IP 和端口号是不能轻易改变的,否则用户端不知道就会连不上服务端
  • 所以现在我们写的需要手动传入服务端的IP和端口号

img

关于客户端的绑定问题

  • 首先由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要绑定各自的IP地址和端口号。
  • 只不过服务端需要指定端口号进行显式地绑定,而客户端不需要显式地绑定端口号。
  • 服务器的IP+端口号需要保证唯一性、固定性和公开性,因此显示绑定端口号就是要将服务器的端口号固定下来。不管服务器重新启动多少次,端口号都不会改变。
  • 而客户端的IP+端口号只要保证唯一性就可以了,端口号不需要进行固定。如果客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端使用,如果被其他程序抢占该客户端就无法联网了。
  • 总之,客户端不需要显示的绑定端口号,在首次发送数据的时候,操作系统会为该客户端进程分配空闲的端口号进行绑定,也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要客户端的端口号没有被耗尽,客户端就永远可以启动

1.2.2 向服务端发送消息

客户端要发送消息给服务端,发送消息的函数是sendto

sendto函数的作用是发送消息

img

send a message on a socket:在套接字上发送消息

函数: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:dest_addr结构体的长度返回值:成功返回写入的字节数,失败返回-1,同时错误码会被设置

第五个参数dest_addr:输入型参数,数据接收方的套接字结构的地址,需要提前填充。(发给谁)

第六个参数addrlen:输入型参数,dest_addr结构体的长度。

我们要做的工作就是定义一个 sockaddr_in 的结构体,然后对该结构体进行内容填充,填完就把给结构体地址传给第五个参数dest_addr,需要强制类型转换


1.2.3 客户端代码

udpClient.hpp

#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;class udpClient
{
public:udpClient(const string &serverip, const uint16_t serverport): _serverip(serverip), _serverport(serverport), _sockfd(-1),  _quit(false){}// 初始化客户端void initClient(){// 创建套接字文件_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd == -1){cerr << "socket error: " << errno << " : " << strerror(errno) << endl;exit(2);}cout << "socket success: " << _sockfd << endl;}// 启动客户端void run(){// 填充数据接收方的套接字结构(服务端)struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_serverport);//主机转网络序列server.sin_addr.s_addr = inet_addr(_serverip.c_str());// 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)string message;while ((!_quit)){cout << "Please Enter# ";cin >> message;sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));}}~udpClient(){}private:uint16_t _serverport; // 端口号string _serverip;     // ip地址int _sockfd;    // 文件描述符bool _quit;
};

udpClient.cc

#include "udpClient.hpp"
#include <memory>// 使用手册
// ./udpClient ip port
static void Uage(string proc)
{cout << "\nUage:\n\t" << proc << " server_ip server_port\n\n";
}int main(int argc, char *argv[])
{if (argc != 3){Uage(argv[0]);exit(1);}// 客户端需要服务端的 IP 和 portstring serverip = argv[1];uint16_t serverport = atoi(argv[2]); // string to intstd::unique_ptr<udpClient> ucli(new udpClient(serverip, serverport));ucli->initClient(); // 初始化服务器ucli->run();        // 启动服务器return 0;
}

二、简单的远程控制程序

2.1 popen、pclose函数

原理:client向server发送shell命令,再由server调用popen执行命令,最后server将命令的执行结果返回给客户端。

popen函数
popen函数是用于创建一个子进程执行命令,并打开一个管道与该进程进行通信。该函数的原型如下:

#include <stdio.h>FILE *popen(const char *command, const char *type);

其中,command参数是一个以null结尾的字符串,包含shell命令来执行。type参数是一个"r"或"w"的字符串,用于指定管道的方向。

popen函数返回一个文件指针(FILE*),这个指针指向由command命令生成的进程的标准输入或标准输出(重定向到管道文件)。调用popen函数会创建一个新的进程,并且返回一个文件指针,可以对其进行读或写操作。

pclose函数

pclose函数是用于关闭由popen函数打开的管道并等待子进程结束。该函数的原型如下:

#include <stdio.h>int pclose(FILE *stream);

其中,stream参数是由popen函数返回的文件指针。pclose函数会等待子进程结束,并返回子进程的终止状态。

popen和pclose函数通常用于在一个进程中执行外部命令,并与该命令进行输入输出的交互。这对于一些需要执行外部命令的操作非常有用,比如执行shell命令并获取输出结果。


2.2 程序代码

对上一个程序的代码做一些小小的改动即可:

udpServer::Start

void Start(){char buffer[1024]; // 网络输入缓冲区std::string mesg;  // 回复给客户端的命令执行结果for (;;){sockaddr_in client;             // 输出型参数socklen_t len = sizeof(client); // 输入输出型参数:输入:client缓冲器的大小,输出:实际读到的client的大小memset(&client, 0, sizeof(client));ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &len);if (s > 0){buffer[s] = 0;// 过滤非法请求if (strcasestr(buffer, "rm") != nullptr){mesg = "坏人... ";mesg += buffer;sendto(_sockfd, mesg.c_str(), mesg.size(), 0, (sockaddr *)&client, len);continue;}// 打印控制命令std::string client_ip = inet_ntoa(client.sin_addr); // 注意inet_ntoa的参数是sin_addr结构uint16_t client_port = ntohs(client.sin_port);printf("[%s:%d]# %s\n", client_ip.c_str(), client_port, buffer);// 执行控制命令FILE *fp = popen(buffer, "r");if (fp == nullptr){LogMessage(ERROR, "(%d)%s", errno, strerror(errno));mesg = "error: (";mesg += std::to_string(errno);mesg += ")";mesg += strerror(errno);sendto(_sockfd, mesg.c_str(), mesg.size(), 0, (sockaddr *)&client, len);continue;}// 收集执行结果char ret[128]; // 临时缓冲区mesg.clear();while (fgets(ret, sizeof(ret), fp) != nullptr){mesg += ret;}// 一定要使用pclose关闭popen返回的文件流pclose(fp);}// 返回控制命令的执行结果sendto(_sockfd, mesg.c_str(), mesg.size(), 0, (sockaddr *)&client, len);}}

udpClient::Run

void Run(){ sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_serverport);server.sin_addr.s_addr = inet_addr(_serverip.c_str());char buffer[1024];while (true){//1.获取命令printf("Please Enter$ ");fgets(buffer, sizeof(buffer), stdin);buffer[strlen(buffer) - 1] = '\0'; // 删除换行符if (strcmp(buffer, "quit") == 0){break;}//2.将命令发送给服务端// 当client首次给server发送数据时,OS会自动给client绑定IP地址和端口号ssize_t s = sendto(_sockfd, buffer, strlen(buffer), 0, (sockaddr *)&server, sizeof(server));if (s == -1){LogMessage(ERROR, "(%d)%s", errno, strerror(errno));continue;}//3.接收服务端返回的执行结果sockaddr_in temp;memset(&temp, 0, sizeof(temp));socklen_t len = sizeof(temp);s = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (sockaddr*)&temp, &len);if(s>0){buffer[s] = 0;printf("server:\n%s\n", buffer);}}}

三、简单的网络聊天室程序

3.1 工作原理

  1. client启动时会自动向server发送一条login消息,此时server根据来源记录用户信息,用户上线;
  2. client将消息发送给server,server再将消息转发给所有的在线用户。
  3. 直到client退出自动向server发送一条quit消息,此时server根据来源删除对应的用户记录,用户下线。
  4. client的读写功能分离,创建两个子线程分别完成读写操作,使得消息的发送和接收功能可以并发执行。

注意:无论是多线程读还是写,使用的_sockfd都是同一个套接字文件描述符。也就是说,UDP协议是全双工的可以同时进行收发而不受干扰。这是因为底层有两个缓冲区,一个是读缓冲区,一个是写缓冲区。


3.2 程序代码

udp_server.hpp

#ifndef __UDP_SEVER_HPP__
#define __UDP_SEVER_HPP__
#include ...class UdpServer
{int _sockfd;uint16_t _port;std::string _ip;std::unordered_map<std::string, sockaddr_in> _users; //在线用户列表public:UdpServer(const std::string &ip, const uint16_t port): _ip(ip),_port(port),_sockfd(-1){}void InitServer(){//同第一个echo程序...}void Start(){char buffer[1024];char username[64];char msg[1024];for (;;){// 接收消息sockaddr_in client;             // 输出型参数socklen_t len = sizeof(client); // 输入输出型参数:输入:client缓冲器的大小,输出:实际读到的client的大小memset(&client, 0, sizeof(client));ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &len);if (s > 0){buffer[s] = 0;std::string client_ip = inet_ntoa(client.sin_addr); // 注意inet_ntoa的参数是sin_addr结构uint16_t client_port = ntohs(client.sin_port);snprintf(username, sizeof(username), "%s:%d", client_ip.c_str(), client_port);snprintf(msg, sizeof(msg), "[%s]# %s", username, buffer);// 添加在线用户if (_users.find(username) == _users.end()){LogMessage(NORMAL, "%s log in!", username);_users.insert({username, client});}printf("%s\n", msg);}// 群发消息for (auto &user : _users){LogMessage(NORMAL, "push message: %s", user.first.c_str());sendto(_sockfd, msg, sizeof(msg), 0, (sockaddr *)&user.second, sizeof(user.second));}// 用户退出,删除在线用户if (strcmp(buffer, "quit") == 0){LogMessage(NORMAL, "%s log out!", username);_users.erase(username);}}}~UdpServer(){if (_sockfd >= 0)close(_sockfd);}
};#endif

udp_client.cc

#include ...int main(int argc, char *argv[])
{std::string serverip = "43.143.194.141";uint16_t serverport = 8080;//创建客户端对象UdpClient udpcli(serverip, serverport);udpcli.InitClient();//创建两个子线程分别完成读写操作,使得消息的发送和接收功能可以并发执行。std::thread t1(&UdpClient::SendMsg, &udpcli);std::thread t2(&UdpClient::RecvMsg, &udpcli);t1.join();t2.join();return 0;
}

udp_client.hpp

#pragma once#include ...class UdpClient
{int _sockfd;std::string _serverip;uint16_t _serverport;bool _quit;public:UdpClient(const std::string &ip, const uint16_t port): _serverip(ip),_serverport(port),_sockfd(-1),_quit(false){}void InitClient(){//同第一个echo程序...}void SendMsg(){sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_serverport);server.sin_addr.s_addr = inet_addr(_serverip.c_str());char buffer[1024] = "login";bool first = true; //client启动时会自动向server发送一条login消息while (!_quit){if (!first){printf("Please Enter$ ");fgets(buffer, sizeof(buffer), stdin);buffer[strlen(buffer) - 1] = '\0'; // 删除换行符}first = false;if (strcmp(buffer, "quit") == 0){_quit = true;}if (strlen(buffer) < 1)continue;// 当client首次给server发送数据时,OS会自动给client绑定IP地址和端口号ssize_t s = sendto(_sockfd, buffer, strlen(buffer), 0, (sockaddr *)&server, sizeof(server));if (s == -1){LogMessage(ERROR, "(%d)%s", errno, strerror(errno));continue;}}}void RecvMsg(){char buffer[1024];while (!_quit){// sockaddr_in temp;// memset(&temp, 0, sizeof(temp));// socklen_t len = sizeof(temp);// ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&temp, &len);ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr);if (s > 0){buffer[s] = 0;fprintf(stderr, "%s\n"); //将接收的的消息打印到标准错误}}}~UdpClient(){if (_sockfd >= 0)close(_sockfd);}
};

为了分屏进行输入和输出,可以将标准错误(2号文件)重定向到其他终端进行显示

在这里插入图片描述

提示:可以在/dev/pts目录下查看其他打开的终端(字符设备文件)

相关文章:

【socket编程】UDP网络通信 {简单的服务器echo程序;简单的远程控制程序;简单的网络聊天室程序}

今天我们通过以下的几个surver/client模型了解一下&#xff35;&#xff24;&#xff30;网络通信 一、简单的服务器echo程序 以下部分内容转载自「网络编程」简单UDP网络通信程序的实现_socket udp-CSDN博客 1.1 服务端 首先明确&#xff0c;这个简单的UDP网络程序分客户端…...

大数据存储解决方案:HDFS与NoSQL数据库详解

大数据存储解决方案&#xff1a;HDFS与NoSQL数据库详解 大数据存储解决方案在现代数据处理和分析中扮演着至关重要的角色。随着数据量的迅猛增长&#xff0c;传统的存储方式已经无法满足需求。HDFS&#xff08;Hadoop分布式文件系统&#xff09;和NoSQL数据库是当前最常用的两…...

如何用 ChatGPT 提升学术写作:15 个高效提示

在本文&#xff0c;我们详细探讨了如何利用 ChatGPT 提升学术写作的各个方面。我们帮助学术作者通过生成创意点子、构建论证结构、克服写作障碍以及格式化引用&#xff0c;从而显著提升其学术论文的质量。这 15 条提示不仅可以单独使用&#xff0c;还可作为学习的良好范例。 本…...

【算法】贪心算法

应用场景——集合覆盖问题 假设存在下面需要付费的广播台&#xff0c;以及广播台信号可以覆盖的地区。如何选择最少的广播台&#xff0c;让所有的地区都可以接收到信号 贪心算法介绍 1.贪心算法是指在对问题进行求解时&#xff0c;在每一步选择中都采取最好或者最优的选择 2…...

常见中间件漏洞复现之【Jboss】!

Jboss介绍 JBoss是⼀个基于J2EE的开发源代码的应⽤服务器。JBoss代码遵循LGPL许可&#xff0c;可以在任何商业应⽤中免费使⽤。JBoss是⼀个管理EJB的容器和服务器&#xff0c;⽀持EJB1.1、EJB 2.0和EJB3的规范。但JBoss核⼼服务不包括⽀持servlet/JSP的WEB容器&#xff0c;⼀般…...

Java常用中间件(后续更新)

常用Java中间件总结 目录 引言什么是中间件常见的Java中间件 1. 消息队列中间件 1.1 RabbitMQ1.2 Apache Kafka 2. 数据库中间件 2.1 MySQL Proxy2.2 Hibernate 3. 服务治理中间件 3.1 Spring Cloud3.2 Dubbo 4. 缓存中间件 4.1 Redis4.2 Ehcache 总结 引言 在现代软件开发…...

网站或者网页Cookie 启用说明

背景说明 有时候登录网站的时候&#xff0c;某些网站的主页会弹出‘Cookie启用’的提示&#xff0c;比较好奇&#xff0c;于是就特别去查询相关资料研究了一下&#xff0c;以下是一个网页demo提示&#xff1a; 说明 Cookie 是一种在 Web 开发中广泛使用的机制&#xf…...

Java 抽象知识笔记总结(油管)

Java系列文章目录 Java Optional 容器笔记总结 文章目录 Java系列文章目录一、前言二、学习内容&#xff1a;三、问题描述四、解决方案&#xff1a;4.1 抽象类的使用4.2 抽象类与接口的区别4.2.1 接口复习4.2.2 具体区别4.2.3 使用场景4.2.3.1 抽象类使用场景4.2.3.2 接口使用…...

鲜花销售小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;农户管理&#xff0c;产品分类管理&#xff0c;农产品管理&#xff0c;咨询信息管理&#xff0c;咨询回复管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&…...

Golang | Leetcode Golang题解之第324题摆动排序II

题目&#xff1a; 题解&#xff1a; func wiggleSort(nums []int) {n : len(nums)x : (n 1) / 2target : quickSelect(nums, x-1)transAddress : func(i int) int { return (2*n - 2*i - 1) % (n | 1) }for k, i, j : 0, 0, n-1; k < j; k {tk : transAddress(k)if nums[t…...

32、Python之面向对象:对象的表示,再论Python是dict包括语法糖

引言 在前面介绍Python容器的时候&#xff0c;我们曾经用过这种夸张的表述&#xff0c;“Python就是包裹在一堆语法糖中的字典”。虽然夸张&#xff0c;其实更多的是为了突出Python中dict的强大之处。今天这篇文章&#xff0c;打算看下Python中类对象、实例对象的表示及内存管理…...

高级java每日一道面试题-2024年8月07日-网络篇-你对TCP的三次握手了解多少?

如果有遗漏,评论区告诉我进行补充 面试官: 你对TCP的三次握手了解多少? 我回答: TCP&#xff08;Transmission Control Protocol&#xff09;的三次握手是TCP建立连接的过程&#xff0c;它是TCP/IP协议族中一个关键的概念。三次握手确保了双方之间的连接是双向的&#xff0…...

vite.config.ts中proxy的rewrite理解

服务器配置都是在开发情况下适用&#xff01;&#xff01; // 服务器配置 server: {//允许IP访问host: "0.0.0.0",//应用端口&#xff08;默认&#xff1a;3000&#xff09;port: Number(env.VITE_APP_PORT),// 运行是否自动打开浏览器open: true,// 代理配置proxy:…...

大数据环境下用户数据隐私安全防护系统的设计与实现(论文+源码)_kaic

摘 要 现如今互联网已在世界范围内广泛的应用和发展&#xff0c;特别是移动互联网Web 技术快速发展&#xff0c;然而最近几年经常发生互联网用户信息泄露及财产损失问题&#xff0c;网络安全漏洞严重威胁Web应用程序安全及互联网用户的网络使用安全&#xff0c;因此现急需一…...

基于springboot+vue+uniapp的“口腔助手”小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…...

算法刷题之链表

// 单链表 struct ListNode {int val; // 节点上存储的元素ListNode *next; // 指向下一个节点的指针ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数 };ListNode* head new ListNode(5); 重要方法&#xff1a;虚拟头节点 个人方法&#xff1a;指针转为数组…...

C# 设计模式之适配器模式

总目录 前言 在实际的开发过程中&#xff0c;由于需求的变化和扩展&#xff0c;我们的代码也需要做相应的扩展。想象这样一个场景&#xff0c;原项目中接口返回的数据是XML格式的数据&#xff0c;但现在来了一个新客户&#xff0c;它期望接口返回的数据类型为json格式的。想要…...

BFS实现迷宫最短路径

结合队列的知识利用 广度优先遍历&#xff0c;通过对能走的路径的记录以及对走过路径的标记&#xff0c;进行多条路搜查 一、理论基础 如下图的迷宫&#xff1a; 选取所走方向&#xff08;针对某一个位置&#xff09;下&#xff0c;右&#xff0c;上&#xff0c;左&#xff0…...

Linux IPC解析:匿名命名管道与共享内存

目录 一.IPC机制介绍二.匿名与命名管道1.匿名管道2.命名管道3.日志 三.共享内存三.System V 标准1.System V简介2.IPC在内核的数据结构设计3.信号量 一.IPC机制介绍 IPC&#xff08;Inter-Process Communication&#xff0c;进程间通信&#xff09;是计算机系统中不同进程之间交…...

Codeforces Round 964 (Div. 4) A~G

封面原图 画师ideolo A - AB Again? 题意 给你一个两位数&#xff0c;把他的个位和十位加起来 代码 #include <bits/stdc.h> using namespace std; typedef long long ll; typedef double db; typedef pair<int,int> pii; typedef pair<ll,ll> pll;voi…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…...

Admin.Net中的消息通信SignalR解释

定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...

练习(含atoi的模拟实现,自定义类型等练习)

一、结构体大小的计算及位段 &#xff08;结构体大小计算及位段 详解请看&#xff1a;自定义类型&#xff1a;结构体进阶-CSDN博客&#xff09; 1.在32位系统环境&#xff0c;编译选项为4字节对齐&#xff0c;那么sizeof(A)和sizeof(B)是多少&#xff1f; #pragma pack(4)st…...

全球首个30米分辨率湿地数据集(2000—2022)

数据简介 今天我们分享的数据是全球30米分辨率湿地数据集&#xff0c;包含8种湿地亚类&#xff0c;该数据以0.5X0.5的瓦片存储&#xff0c;我们整理了所有属于中国的瓦片名称与其对应省份&#xff0c;方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?

论文网址&#xff1a;pdf 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#xff01;文章偏向于笔记&#xff0c;谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...

在四层代理中还原真实客户端ngx_stream_realip_module

一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡&#xff08;如 HAProxy、AWS NLB、阿里 SLB&#xff09;发起上游连接时&#xff0c;将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后&#xff0c;ngx_stream_realip_module 从中提取原始信息…...

Linux云原生安全:零信任架构与机密计算

Linux云原生安全&#xff1a;零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言&#xff1a;云原生安全的范式革命 随着云原生技术的普及&#xff0c;安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测&#xff0c;到2025年&#xff0c;零信任架构将成为超…...

实现弹窗随键盘上移居中

实现弹窗随键盘上移的核心思路 在Android中&#xff0c;可以通过监听键盘的显示和隐藏事件&#xff0c;动态调整弹窗的位置。关键点在于获取键盘高度&#xff0c;并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...

【JavaWeb】Docker项目部署

引言 之前学习了Linux操作系统的常见命令&#xff0c;在Linux上安装软件&#xff0c;以及如何在Linux上部署一个单体项目&#xff0c;大多数同学都会有相同的感受&#xff0c;那就是麻烦。 核心体现在三点&#xff1a; 命令太多了&#xff0c;记不住 软件安装包名字复杂&…...

Git常用命令完全指南:从入门到精通

Git常用命令完全指南&#xff1a;从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...