【Linux】UDP的服务端 + 客户端

文章目录

  • 📖 前言
  • 1. TCP和UDP
  • 2. 网络字节序
    • 2.1 大小端字节序:
    • 2.2 转换接口:
  • 3. 服务端socket
    • 3.1 成员变量:
    • 3.2 socket接口:
    • 3.3 sockaddr结构:
    • 3.4 配置sockaddr_in:
    • 3.5 inet_addr:
    • 3.6 inet_ntoa:
    • 3.7 bind绑定:
  • 4. 服务端start
    • 4.1 recvfrom:
    • 4.2 sendto:
  • 5. 客户端
  • 6. 测试
    • 6.1 本地回环地址:
  • 7. Windows客户端

📖 前言

从上一章开始我们正式进入Linux网络编程的学习,上回中我们对网络有了大概的认识,宏观上了解了网络的传输过程,对局域网广域网以及Mac地址和IP地址有了初步的认识。
本章我们正式进入网络编程,用代码来实现网络间的通信,学习认识相关的接口……

代码详情:👉 Gitee


1. TCP和UDP

为了完成通信,传输层有两个重要的协议。

  • TCP:Transmission Control Protocol 传输控制协议
  • 传输层协议:
    • 在网络通信中负责提供端到端的数据传输服务的协议。
  • 有链接:
    • 在数据传输之前,发送方和接收方需要建立一个可靠的连接。
  • 可靠传输:
    • 通过一系列的机制和算法来确保数据能够完整、准确地传输到目标主机,并且按照正确的顺序进行重组和接收。
  • 面向字节流:
    • 是一种数据传输的方式,其中数据被视为一连串的字节序列。
  • UDP:User Datagram Protocol 用户数据报协议
  • 传输层协议:
    • 在网络通信中负责提供端到端的数据传输服务的协议。
  • 无连接:
    • 无连接指的是数据传输时不需要先建立连接再进行通信的方式,比如所有人都能给你的电子邮箱发送邮件。
  • 不可靠传输:
    • 在不可靠传输中,数据传输过程中不进行可靠性保证的方式,发送方将数据发送给接收方,但不对数据的正确性和完整性进行确认和修复。
  • 面向数据报:
    • 数据在传输过程中被划分为独立的数据报进行传输,每个数据报(也称为包、帧等)都包含了完整的源地址、目标地址和其他必要的信息,使得每个数据报都能够独立地进行路由和处理。

可靠与不可靠传输,更多的标明的是一种通信特征。不能说tcpudp哪个更好,只能说哪个更合适。


2. 网络字节序

我们之前学过C语言都知道,有大端机和小端机,那么不同的计算机的字节序,要向网上发,其他计算结接收时,不知道发过来的数据是按照大端还是小端字节序来读,所有必须要有统一的规定。

规定网络字节序列是一种大端序列。

  • 要保证发到网络中的序列必须是大端的。
  • 无论是发送方还是接收方,都要直接或者间接的将自己的数据由主机序列转成网络序列,或者由网络序列转成主机序列。

2.1 大小端字节序:

  • 大端字节序(Big-endian):
  • 是指将高位字节存储在低地址,低位字节存储在高地址的方式。
  • 例如,十六进制数0x12345678在大端字节序下的内存存储方式为0x12 0x34 0x56 0x78
  • 小端字节序(Little-endian):
  • 是指将低位字节存储在低地址,高位字节存储在高地址的方式。
  • 例如,十六进制数0x12345678在小端字节序下的内存存储方式为0x78 0x56 0x34 0x12

2.2 转换接口:

在这里插入图片描述
如果主机就是大端机,这些函数什么都不会做。主机是小端机,则会将主机字节序转换成网络字节序(大端字节序),网络字节序转主机字节序也是同样的道理。


3. 服务端socket

3.1 成员变量:

class UdpServer
{
private:// 服务器必须得有端口号信息uint16_t port_;// 服务器必须得有ip地址std::string ip_;// 服务器的socket fd信息int sockfd_;// onlineuserstd::unordered_map<std::string, struct sockaddr_in> users;
};

3.2 socket接口:

在这里插入图片描述
在这里插入图片描述

  • Socket(套接字)是一种用于网络通信的编程接口和抽象概念,它使得应用程序可以通过标准的流式或数据报式方式发送和接收数据包,实现不同计算机之间的通信。
  • 在计算机网络中,一个 Socket 由 IP 地址和端口号两部分组成
    • IP 地址用于标识网络上的一个主机,而端口号则用于标识该主机上的一个进程。
    • Socket 通过 IP 地址和端口号来唯一确定一个网络上的进程,并且可以通过多种协议(如 TCP、UDP)进行数据传输。
  • 使用 Socket 接口,应用程序可以直接访问网络协议栈,与其他主机建立连接并进行数据交互。因此,Socket 是实现各种协议和服务的基础,如 HTTP、SMTP、FTP、Telnet 等。

返回值:

在这里插入图片描述

Linux下的一切皆文件包括了socket接口。每个打开的文件(包括socket)都会被分配一个文件描述符(file descriptor),它是一个非负整数。

  • 使用socket API 创建一个socket时,会返回一个socket文件描述符(socket fd),我们可以通过这个socket fd来进行网络的发送和接收操作。实际上,发送和接收数据就像对文件写入和读取数据一样操作。
  • 发送数据时,我们可以使用类似于写入文件的操作,使用write()函数将数据写入到socket fd中。
  • 接收数据时,我们可以使用类似于读取文件的操作,使用read()函数从socket fd中读取数据。
  • 此外,还可以使用其他文件操作函数,如open()、close()、select()等,对socket进行更复杂的操作。

API:

API 是 Application Programming Interface 的缩写,翻译为应用程序编程接口。是一组定义了不同软件组件之间,相互通信和交互的规范和工具集合。它允许不同的软件系统应用程序或服务之间进行数据传递、功能调用和交互操作。

那么socket是打开了一个文件吗:

  • 在Linux中,socket并不是打开一个文件,而是提供了一种抽象的接口,用于进行网络通信。
  • 尽管在编程上可以将socket看作是一个文件描述符,但实际上并没有打开一个物理文件。
  • 当我们调用socket()函数创建一个socket时,操作系统会为该socket分配资源,并返回一个文件描述符(socket fd)。
  • 这个文件描述符是一个整数值,用于标识该socket。

3.3 sockaddr结构:

Socket 是一种抽象层,提供了一种通用的应用程序编程接口(API),允许应用程序通过网络或本地主机之间进行通信。它可以用于不同协议的网络通信,包括 TCP、UDP 等。

除了在网络通信中使用外,它还可以用于同一台计算机上的应用程序之间的通信,例如进程间通信、线程间通信等。

那么一个接口干两件事,如何区分呢?

  • sockaddr,用来接收目标信息,这个值的参数可以是sockaddr_in/scokaddr_un/sockadd_in6之中的任意一个(需要强转指针)。

sockaddr是一个通用的地址结构体,它主要用于在网络编程中传递和表示套接字地址。在实际使用中,我们通常会使用sockaddr的具体派生结构体,例如sockaddr_in(IPv4)或sockaddr_in6(IPv6)或scokaddr_un,它们在sockaddr的基础上添加了特定的字段,以方便使用不同类型的套接字地址。
在这里插入图片描述
sockaddr_in、sockaddr_un和sockaddr_in6sockaddr结构体的几个具体实现,用于在网络编程中表示不同类型的套接字地址:

  • 其中,sockaddr_in结构体用于表示IPv4地址,包括一个16位端口号和一个32位IP地址。该结构体 “继承” 自sockaddr结构体,并且增加了专门存储端口号和IP地址的字段。
  • sockaddr_un结构体用于表示UNIX域套接字的地址,包括UNIX域套接字的路径名。该结构体同样 “继承” 自sockaddr结构体,并且增加了存储路径名的字段。
  • sockaddr_in6结构体用于表示IPv6地址,该结构体也同样 “继承” 自sockaddr结构体,而其增加了专门存储IPv6地址和端口号的字段。
  • 在使用这些结构体时,可以根据需要将它们强制转换为sockaddr结构体使用,以便在函数调用中进行传递。

补充:

  • 相同起始成员: sockaddr结构体和这些特定结构体都有名为 “sa_family” 的成员变量,用于指示地址家族。这个成员在不同的特定结构体中具有相同的位置和作用。
  • 强制类型转换: 因为这些特定结构体的首部成员与sockaddr结构体的首部成员相同,并且只有首部成员是重要的,所以可以通过将特定结构体的指针强制转换为sockaddr结构体的指针,并传递给需要sockaddr结构体参数的函数。

3.4 配置sockaddr_in:

因为用的是ipv4的网络通信,所以这里需要初始化一个sockaddr_in类型的结构体:

// 绑定网络信息,指明ip + port
// 先填充基本信息到 struct sockaddr_in
struct sockaddr_in local;
bzero(&local, sizeof(local));// 清空操作

首先是把协议家族设置为IPV4,端口配置为代码所在函数参数中传的端口号:

// 填充协议家族,域
local.sin_family = AF_INET;
// 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中
local.sin_port = htons(port_);

这个local.sin_family就是前16位,确定是本地通信还是网络通信,也可以用PF_INET是一样的。

然后配置IP:

local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());
  • 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制,4字节IP,uint32_t ip
  • INADDR_ANY(0):程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法。

3.5 inet_addr:

inet_addr()函数可以将一个点分十进制的IPv4地址转换为网络字节序,下的32位二进制整数,即4个字节的IP地址。

in_addr_t inet_addr(const char *cp);

因为对于网络来说并不认识字符串类型的ip,只认识网络字节流规定的ip。

IPV4地址是由四个十进制数组成(每个十进制数的数值是8位二进制数的数值),每个数组表示一个字节,范围从0~255,用点分十进制表示。

  • 例如,123.123.0.1是一个IPV4地址。
  • 每个区域都是8个比特位一字节的数据。

in_addr_t转到定义就是uint32_t
在这里插入图片描述

ip第四个字节,用的是位段来存储的:

// 示例
struct ip
{uint32_t part1:8;uint32_t part2:8;uint32_t part3:8;uint32_t part4:8;
}

3.6 inet_ntoa:

inet_ntoa()函数将网络请求中的IP地址转换为字符串类型,接受一个struct in_addr类型的参数,该类型表示一个IPv4地址。

从网络请求中获取到的IP地址转换为struct in_addr类型,然后再使用inet_ntoa函数将其转换为字符串类型。

char *inet_ntoa(struct in_addr in);

很多同学不知道struct in_addr是什么类型,我们不妨在vscode中点开struct sockaddr_in类型定义来看看:

在这里插入图片描述
所以我们在传参时,只需要将struct sockaddr_in类的对象的成员传过去就好了。

返回值是个char*类型的,那么字符串在哪呢?

  • inet_ ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。
  • 那么这块内存需要我们手动释放吗?答案是不需要!
  • 它会返回一个静态申请的buffer
  • man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放。
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>int 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);std::cout << "ptr1: " << ptr1 << " " << "ptr2: " << ptr2 << std::endl;return 0;
}

在这里插入图片描述

在APUE中, 明确提出inet_ntoa不是线程安全的函数。但是在Centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁。

3.7 bind绑定:

在这里插入图片描述
该接口是指定socketsockaddr进行绑定,第三个参数是addr参数的大小。

// bind 网络信息 -- 将数据填入到操作系统里
if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
{logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);exit(2);
}

在之前的初始化struct sockaddr_in时,我们提到过INADDR_ANY

  • INADDR_ANY(0):程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法。

在这里插入图片描述
INADDR_ANY:转到定义上去看,我们发现它就是0。
在这里插入图片描述
关于端口号,不要绑定,0到1023以前的端口号,是服务器或者特定服务用的,一绑定可能就出错了:

在这里插入图片描述
绑定完之后,我们的服务器就配置成功了!

小结一下:(个人理解)

  • sockaddr存储着套接字的信息,bindsockaddrsocket绑定起来,然后socket函数去处理套接字。

具体代码如下:

// udp服务器,只需要,1. 创建套接字 2. 填充信息之后做绑定,绑定完成之后就算完成
void init()
{// 1. 创建socket套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 就是打开了一个文件if (sockfd_ < 0){logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);exit(1);}// 日志logMessage(DEBUG, "socket create success: %d", sockfd_);// 2. 绑定网络信息,指明ip + port// 2.1 先填充基本信息到 struct sockaddr_instruct sockaddr_in local;     // local在哪里开辟的空间? 用户栈,就是临时变量,我们要将其写入内核中bzero(&local, sizeof(local)); // 也可以用memset// 填充协议家族,域local.sin_family = AF_INET;   // 这个family就是前16位,确定是本地通信还是网络通信,也可以用PF_INET是一样的// 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中local.sin_port = htons(port_);// 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制 -> 4字节IP -> uint32_t ip// INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法// inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n (主机转网络)local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());// 2.2 bind 网络信息 -- 将数据填入到操作系统里if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1){logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);exit(2);}logMessage(DEBUG, "socket bind success: %d", sockfd_);
}

4. 服务端start

4.1 recvfrom:

在这里插入图片描述
recvfrom函数用于接收UDP协议的数据报,它从指定的文件描述符处读取数据,并将数据保存在指定的缓冲区buf中,同时将发送方的地址信息存储在addr参数所指向的结构体中。

返回值:

在这里插入图片描述

  • 如果接收成功,并且收到了数据,则返回接收到的数据的字节数。
  • 如果连接关闭,即对方套接字(socket)关闭连接,则返回0。
  • 如果发生错误,返回-1,并且可以使用errno变量获取具体的错误码。

start具体实现:

void start()
{char inbuffer[1024];  // 将来读取到的数据,都放在这里char outbuffer[1024]; // 将来发送的数据,都放在这里// 服务器设计的时候,服务器都是死循环while (true){// 远端struct sockaddr_in peer;      // 输出型参数socklen_t len = sizeof(peer); // 输入输出型参数// demo2// UDP是无连接的// 对方给你发了消息,你想不想给对方回消息?// 要的!后面的两个参数是输出型参数,发消息的一方会将属性写到对应的peer和len当中// 不断地从网络当中进行数据读取:ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0,(struct sockaddr *)&peer, &len);// 数据已经读到了吧if (s > 0){inbuffer[s] = 0; // 当做字符串}else if (s == -1){logMessage(WARINING, "recvfrom: %s:%d", strerror(errno), sockfd_);continue;}// 谁发的消息,将对方的信息提取出来:// 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]std::string peerIp = inet_ntoa(peer.sin_addr);  // 拿到了对方的IPuint32_t peerPort = ntohs(peer.sin_port);       // 拿到了对方的port// 打印出来客户端给服务器发送过来的消息logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);for (int i = 0; i < strlen(inbuffer); i++){if(isalpha(inbuffer[i]) && islower(inbuffer[i]))outbuffer[i] = toupper(inbuffer[i]);elseoutbuffer[i] = toupper(inbuffer[i]);}// 谁给我发消息,立马转回去sendto(sockfd_, outbuffer, strlen(outbuffer), 0, (struct sockaddr*)&peer, len);}
}

4.2 sendto:

在这里插入图片描述
sendto函数用于通过UDP协议发送数据报。它可以将指定的缓冲区中的数据发送到目标地址。

在我们上述start函数中,我们还实现了一个功能就是将收到的信息处理之后,再发回出去。客户端可以再用recvfrom接到消息,再显示出来。

服务类的成员变量和main函数:

// 使用手册
static void Usage(const std::string porc)
{std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}/// @brief  我们想写一个简单的udpSever
/// 云服务器有一些特殊情况:
/// 1. 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意
class UdpServer
{
public:UdpServer(int port, std::string ip = "") : port_((uint16_t)port), ip_(ip), sockfd_(-1){}~UdpServer(){}// .........private:// 服务器必须得有端口号信息uint16_t port_;// 服务器必须得有ip地址std::string ip_;// 服务器的socket fd信息int sockfd_;// onlineuserstd::unordered_map<std::string, struct sockaddr_in> users;
};// ./udpServer port [ip]
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3) // 反面:argc == 2 || argc == 3{Usage(argv[0]);exit(3);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}UdpServer svr(port, ip);svr.init();svr.start();return 0;
}

5. 客户端

有了上述知识,客户端的实现就一马平川了。

struct sockaddr_in server;static void Usage(std::string name)
{std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}void *recverAndPrint(void *args)
{while (true){int sockfd = *(int *)args;char buffer[1024];struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl;}}
}// ./udpClient server_ip server_port
// 如果一个客户端要连接server必须知道server对应的ip和port
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}// 1. 根据命令行,设置要访问的服务器IPstd::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);// 2. 创建客户端// 2.1 创建socket,服务器是udp的已经跑起来了,客户端也要想办法去连接服务器// 所以客户端也必须得有套接字信息int sockfd = socket(AF_INET, SOCK_DGRAM, 0);assert(sockfd > 0);// 2.2 填写服务器对应的信息bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());// 创建一个线程就可以了// pthread_t t;// pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);// 3. 通讯过程std::string buffer;while (true){std::cerr << "Please Enter# ";std::getline(std::cin, buffer);// 发送消息给server:// 客户端首次调用sendto函数的时候,我们的client会自动bind自己的ip和portsendto(sockfd, buffer.c_str(), buffer.size(), 0,\(const struct sockaddr *)&server, sizeof(server)); // 发完消息之后再转发回去char buffer[1024];struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl; }}close(sockfd);return 0;
}

客户端需不需要bind???是需要bind的,但是不需要用户自己bind,而是OS自动bind的!!!

  • 所谓的 “不需要” ,指的是:不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!
  • 如果我非要自己bind呢???可以!严重不推荐!!!
  • 所有的客户端软件,服务器在进行通信的时候,必须得有 client[ip:port] <-> server[ip:port]为什么呢??
    • 因为client很多,不能给客户端bind指定的port,port可能已经被别的client使用了。
    • 一旦被别的client使用了你的client就无法启动了,因为一个客户端只能被一个进程绑定。
    • 只能让操作系统随机生成端口号,用的时候拿去用,不用了就回收掉,下次客户端再来再把端口号给别的客户端。

那么server凭什么要bind呢??

  • server提供的服务,必须被所有人知道!server不能随便改变!
  • client端口号是多少一点都不重要,只需要保证唯一性就可以,因为没人连它。

在填写好服务端主机的信息之后,客户端直接就可以向服务端发送消息,main函数:

// ./udpServer port [ip]
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3) // 反面:argc == 2 || argc == 3{Usage(argv[0]);exit(3);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}UdpServer svr(port, ip);svr.init();svr.start();return 0;
}

6. 测试

在测试之前,我们先把日志实现一下:

#pragma once#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);char *name = getenv("USER");char logInfo[1024];va_list ap; // ap就是一个char*类型va_start(ap, format);vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);va_end(ap); // ap = NULLFILE *out = (level == FATAL) ? stderr:stdout;fprintf(out, "%s | %u | %s | %s\n", \log_level[level], \(unsigned int)time(nullptr),\name == nullptr ? "unknow":name,\logInfo);
}

6.1 本地回环地址:

127.0.0.1是IPv4地址中的本地回环地址。它通常被称为“localhost”,可用于测试计算机或网络设备的网络功能,以及运行并测试网络应用程序等。当计算机尝试连接到127.0.0.1时,它实际上是在尝试与自己本身通信,因此这个地址非常有用。

客户端发的消息,经过网络协议栈,不往网络里发,到了网络协议栈的最底部,再由最底部向上交付。再交付给另一个进程对应的缓冲区里面,让那个进程读到消息。

在这里插入图片描述
加上服务端收到信息后,再将字符窜改为大写再转发回去:

在这里插入图片描述
我们可以创建一个多人聊天室,将所有人的信息(ip + port)都保存在unordered_map中,只要有用户连到主机上,就将其添加到哈希表中。

void checkOnlineUser(std::string &ip, uint32_t port, struct sockaddr_in &peer)
{std::string key = ip;key += ":";key += std::to_string(port);auto iter = users.find(key);if(iter == users.end()){users.insert({key, peer});}else{// iter->first, iter->second->// do nothing}
}

并且将收到的信息群发给所有的用户:

void messageRoute(std::string ip, uint32_t port, std::string info)
{std::string message = "[";message += ip;message += ":";message += std::to_string(port);message += "]# ";message += info;// 给每个在线用户都发回去for(auto &user : users){sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));}
}

UDP的sendto和recvfrom是阻塞式的,sendto会一直阻塞直到数据成功发送或者发生错误,所以我们要加多线程:

void *recverAndPrint(void *args)
{while (true){int sockfd = *(int *)args;char buffer[1024];struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl;}}
}

加了多线程之后,客户端有两个线程,主个线程在发消息, 获取用户输入,发消息;新线程在不断地收消息,并且打印到显示器上去。

为了让输出的内容更直观的显示出来,我们将客户端输出的内容写入到命名管道fifo中(注意:命名管道要现将读端打开,所以先要cat < fifo然后再在服务端发送消息)

在这里插入图片描述
客户端的cout 本来是向显示器打印的,结果被重定向到了fifo命名管道中,重定向只是改变了输出流的目标,将输出内容发送到指定的管道文件中,但不会影响管道中已有的内容,所以才会出现上述情况(原有的信息依旧显示的情况)。

备注:

  • 在 shell 命令中,将输出重定向到 FIFO 命名管道时,先前存在于管道中的数据不会被删除或清空,而是会被保留
  • 与重定向到普通文件不同,FIFO 命名管道是一种特殊的文件类型,用于进程间通信。
  • 当写入进程写入数据时,读取进程可以从管道中读取数据。

当将命令的输出重定向到一个 FIFO 命名管道时,它会将输出写入到管道中,并且不会影响管道中现有的任何数据。


7. Windows客户端

#pragma warning(disable:4996)#include <iostream>
#include <string>
#include <cstdio>
#include <cassert>
#include <WinSock2.h>#pragma comment(lib, "Ws2_32.lib")int server_port = 8080;
std::string server_ip = "xxx.yyy.zz.mm";int main()
{WSADATA data;(void)WSAStartup(MAKEWORD(2, 2), &data);(void)data;SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);assert(sockfd > 0);// 2.2 填写服务器对应的信息struct sockaddr_in server;memset(&server, 0, sizeof server);server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());// 3. 通讯过程std::string buffer;while (true){std::cerr << "Please Enter# ";std::getline(std::cin, buffer);// 发送消息给server:// 客户端首次调用sendto函数的时候,我们的client会自动bind自己的ip和portsendto(sockfd, buffer.c_str(), buffer.size(), 0, (const struct sockaddr*)&server, sizeof(server));char buffer[1024];struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl;}}closesocket(sockfd);WSACleanup();system("pause");return 0;
}

除了开头一些Windows需要的东西外,其他的与Linux下的代码一模一样,这样我们就可以在Windows端来访问部署在Linux下的服务了。

在这里插入图片描述

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

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

相关文章

吃鸡高手必备工具大揭秘!提高战斗力,分享干货,一站满足!

大家好&#xff01;你是否想提高吃鸡游戏的战斗力&#xff0c;分享顶级的游戏作战干货&#xff0c;方便进行吃鸡作图和查询装备皮肤库存&#xff1f;是否也担心被骗&#xff0c;希望查询游戏账号是否在黑名单上&#xff0c;或者查询失信人和VAC封禁情况&#xff1f;在这段视频中…

【网络安全 --- 工具安装】VMware 16.0 详细安装过程(提供资源)

一&#xff0c;VMware下载地址&#xff1a; 百度网盘链接链接&#xff1a;百度网盘 请输入提取码百度网盘为您提供文件的网络备份、同步和分享服务。空间大、速度快、安全稳固&#xff0c;支持教育网加速&#xff0c;支持手机端。注册使用百度网盘即可享受免费存储空间https:/…

leetcode - 双周赛114

一&#xff0c;2869.收集元素的最小操作次数 // 解法&#xff1a;哈希表 从右往左遍历 class Solution {public int minOperations(List<Integer> nums, int k) {Set<Integer> set new HashSet<>();for(int i1; i<k; i){set.add(i);}for(int inums.size…

AI:08-基于深度学习的车辆识别

随着汽车行业的迅速发展,车型识别在交通管理、智能驾驶和车辆安全等方面变得越来越重要。基于深度学习的车型识别技术为实现高效准确的车辆分类和检测提供了强大的工具。本文将介绍如何利用深度学习技术来实现车型识别,并提供相应的代码示例。 数据收集和预处理: 为了训练…

如何使用 Hotshot 通过文字生成 GIF 动画

Hotshot 是一个基于人工智能的工具&#xff0c;可用于通过文字生成 GIF 动画。该工具使用最新的图像生成技术来创建逼真的动画&#xff0c;即使是复杂的文字描述也能做到。 hotshot访问地址 使用 Hotshot 生成 GIF 动画 要使用 Hotshot 生成 GIF 动画&#xff0c;您需要首先…

互联网Java工程师面试题·Memcached 篇·第二弹

目录 10、memcached 如何实现冗余机制&#xff1f; 11、memcached 如何处理容错的&#xff1f; 12、如何将 memcached 中 item 批量导入导出&#xff1f; 13、如果缓存数据在导出导入之间过期了&#xff0c;您又怎么处理这些数据呢&#xff1f; 14、memcached 是如何做身份…

Promise, async, await 学习

异步编程简介&#xff1a; 介绍&#xff1a;异步编程是一种编程范式&#xff0c;旨在提高程序的性能和响应能力。该模型允许程序在执行某些任务时&#xff0c;不必等待这些任务完成就可以进行下一步操作&#xff0c;从而提高了程序的效率。 作用&#xff1a;异步编程通常用于…

Python常用功能的标准代码

后台运行并保存log 1 2 3 4 5 6 7 8 9 nohup python -u test.py > test.log 2>&1 & #最后的&表示后台运行 #2 输出错误信息到提示符窗口 #1 表示输出信息到提示符窗口, 1前面的&注意添加, 否则还会创建一个名为1的文件 #最后会把日志文件输出到test.log文…

使用python-opencv检测图片中的人像

最简单的方法进行图片中的人像检测 使用python-opencv配合yolov3模型进行图片中的人像检测 1、安装python-opencv、numpy pip install opencv-python pip install numpy 2、下载yolo模型文件和配置文件&#xff1a; 下载地址&#xff1a; https://download.csdn.net/down…

集群化环境前置准备

目录 部署 1. 配置多台Linux虚拟机 1.1 首先&#xff0c;关机当前CentOS系统虚拟机&#xff08;可以使用root用户执行init 0来快速关 机&#xff09; 1.2 新建文件夹 1.3 克隆 1.4 同样的操作克隆出&#xff1a;node2和node3 1.5 开启node1&#xff0c;修改主机名为node1&…

活动报名与缴费小程序开发笔记一

项目背景 活动报名与缴费小程序的开发背景主要源于以下几个因素&#xff1a; 1.数字化时代的需求&#xff1a; 随着移动互联网和智能手机的普及&#xff0c;人们习惯使用手机进行各种活动。传统的纸质报名表格和线下缴费方式变得相对繁琐&#xff0c;而数字化报名与缴费小程序…

【小沐学前端】Node.js实现基于Protobuf协议的UDP通信(UDP/TCP)

文章目录 1、简介1.1 node1.2 Protobuf 2、下载和安装2.1 node2.2 Protobuf2.2.1 安装2.2.2 工具 3、node 代码示例3.1 HTTP3.2 UDP单播3.4 UDP广播 4、Protobuf 代码示例4.1 例子: awesome.proto4.1.1 加载.proto文件方式4.1.2 加载.json文件方式4.1.3 加载.js文件方式 4.2 例…

gorm 自定义时间、字符串数组类型

文章目录 自定义时间类型自定义字符串数组测试与完整代码测试代码测试结果 GORM 是GO语言中一款强大友好的ORM框架&#xff0c;但在使用过程中内置的数据类型不能满足以下两个需求&#xff0c;如下&#xff1a; time.Time类型返回的是 2023-10-03T09:12:08.5352808:00这种字符串…

FreeRTOS入门教程(队列详细使用示例)

文章目录 前言一、队列基本使用二、如何分辨数据源三、传输大块数据总结 前言 上篇文章我们已经讲解了队列的概念和队列相关的API函数&#xff0c;那么本篇文章的话就开始带大家来学习使用队列。 一、队列基本使用 这个例子将会创建三个任务&#xff0c;其中两个任务用来发送…

完美清晰,炫酷畅享——Perfectly Clear Video为你带来卓越的AI视频增强体验

在我们日常生活中&#xff0c;我们经常会拍摄和观看各种视频内容&#xff0c;无论是旅行记录、家庭聚会还是商务演示&#xff0c;我们都希望能够呈现出最清晰、最精彩的画面效果。而现在&#xff0c;有一个强大的工具可以帮助我们实现这一目标&#xff0c;那就是Perfectly Clea…

谁“动”了我的信息?

通信公司“内鬼” 批量提供手机卡 超6万张手机卡用来发涉赌短信 2023年10月2日&#xff0c;据报道2022年12月&#xff0c;湖北省公安厅“雷火”打击整治治安突出问题专项行动指挥部研判发现&#xff0c;有人在湖北随州利用虚拟拨号设备GOIP发出大量赌博短信。随州市公安局研判…

【数据结构--八大排序】之快速排序

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

Flink--9、双流联结(窗口联结、间隔联结)

星光下的赶路人star的个人主页 我还有改变的可能性&#xff0c;一想起这点&#xff0c;我就心潮澎湃 文章目录 1、基于时间的合流——双流联结&#xff08;Join&#xff09;1.1 窗口联结&#xff08;Window Join&#xff09;1.2 间隔联结&#xff08;Interval Join&#xff09;…

苹果手机怎么备份所有数据?2023年iPhone 15数据备份常用的3种方法!

当苹果手机需要进行刷机、恢复出厂设置、降级iOS系统等操作时&#xff0c;我们需要将自己的iPhone数据提前进行备份。 特别是在苹果发布新iOS系统时&#xff0c;总有一些小伙伴因为升降级系统&#xff0c;而导致了重要数据的丢失。 iPhone中储存着重要的照片、通讯录、文件等数…

出去重复的列值(关键词:distinct)

MySQL从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129334507?spm1001.2014.3001.5502 语法格式: select distinct 列名 from 表名; 案例&#xff1a;查询emp表中&#xff0c;员工的职位&#xff08;job&#xff09;&#xff0c;并去重…