文章目录
- 📖 前言
- 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 用户数据报协议
- 传输层协议:
-
- 在网络通信中负责提供端到端的数据传输服务的协议。
- 无连接:
-
- 无连接指的是数据传输时不需要先建立连接再进行通信的方式,比如所有人都能给你的电子邮箱发送邮件。
- 不可靠传输:
-
- 在不可靠传输中,数据传输过程中不进行可靠性保证的方式,发送方将数据发送给接收方,但不对数据的正确性和完整性进行确认和修复。
- 面向数据报:
-
- 数据在传输过程中被划分为独立的数据报进行传输,每个数据报(也称为包、帧等)都包含了完整的源地址、目标地址和其他必要的信息,使得每个数据报都能够独立地进行路由和处理。
可靠与不可靠传输,更多的标明的是一种通信特征。不能说tcp
和udp
哪个更好,只能说哪个更合适。
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_in6
是sockaddr
结构体的几个具体实现,用于在网络编程中表示不同类型的套接字地址:
- 其中,
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绑定:
该接口是指定socket
和sockaddr
进行绑定,第三个参数是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
存储着套接字的信息,bind
将sockaddr
和socket
绑定起来,然后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下的服务了。