文章目录
一、预备知识1.1端口号1.2传输层的TCP协议与UDP协议TCP协议UDP协议 1.3网络字节序 二、socket编程接口2.1 socket常见API2.2 sockaddr结构 三、简单的UDP网络程序3.1创建UDP套接字3.2服务端绑定字符串IP & 整数IP 3.3运行3.4简易echo服务器实现
一、预备知识
1.1端口号
上网的行为一般可以归结为两种:
把远端的数据拉取到本地;把本地的数据推送到远端。数据拉取到本地的过程我们可以理解为输入,数据推送到远端的过程我们可以理解为输出。
所以,上网的本质就是IO,再具体点,网络通信的本质就是进程间通信。
进程间通信的前提是让不同的进程看到同一份公共资源,很明显这个公共资源就是网络。
那么如何在茫茫网络中找到两个进程呢?
IP(IP地址)+port(端口号)=互联网中唯一的一个进程。
IP地址可以让我们在互联网中找到唯一的一台主机。port端口号可以让我们找到这台主机上唯一的一个进程。端口号(port)的作用实际就是标识一台主机上的一个进程。
端口号是传输层协议的内容。端口号是一个2字节16位的整数。端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。一个进程可以绑定多个端口号,但是一个端口号不能被多个进程同时绑定。我们可以将port端口号与进程绑定,这样进程就可以通过端口号来唯一标识了。
为什么不使用进程ID实现这部分功能?
专事专办,虽然进程ID也能够唯一区分进程,但是这毕竟分属了两个领域:操作系统和网络,你可以理解为有一部分解耦的因素,同时你也应该意识到专事专办的思想是一种正确的系统设计思维。
底层如何通过port找到对应进程的?
实际底层采用哈希的方式建立了端口号和进程PID或PCB之间的映射关系,当底层拿到端口号时就可以直接执行对应的哈希算法,然后就能够找到该端口号对应的进程。
1.2传输层的TCP协议与UDP协议
TCP协议
TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法。
UDP协议
UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议。
使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议无法处理。
有关TCP协议和UDP协议的可靠性问题:
UDP协议不可靠性并不是一种缺点,因为TCP协议对于数据传输错误等情况可以做出处理就意味着TCP协议更复杂,实现了更多的接口,而UDP协议也必定更为简单。
所以这两种协议并不好坏之分,只是区别于使用场景,比如TCP协议适用远程登录:SSH(安全外壳协议)和Telnet(远程登录协议)使用TCP协议来确保远程登录会话的可靠性和安全性。
而UDP协议适用于流媒体传输:如在线视频和音频播放等应用,需要快速的数据传输和低延迟,但对数据的完整性和准确性要求不高。在这些场景下,即使部分数据丢失或出错,也不会对用户体验产生太大影响。
1.3网络字节序
计算机在存储数据时是有大小端的概念的:
大端模式: 数据的高字节内容保存在内存的低地址处,数据的低字节内容保存在内存的高地址处。小端模式: 数据的高字节内容保存在内存的高地址处,数据的低字节内容保存在内存的低地址处。如果编写的程序只在本地机器上运行,那么是不需要考虑大小端问题的,因为同一台机器上的数据采用的存储方式都是一样的,要么采用的都是大端存储模式,要么采用的都是小端存储模式。但如果涉及网络通信,那就必须考虑大小端的问题,否则对端主机识别出来的数据可能与发送端想要发送的数据是不一致的。
而TCP/IP协议解决这一问题的方式非常简单:规定网络数据流应采用大端字节序,即低地址高字节。
如果发送端是小端,需要先将数据转成大端,然后再发送到网络当中。如果发送端是大端,则可以直接进行发送。如果接收端是小端,需要先将接收到数据转成小端后再进行数据识别。如果接收端是大端,则可以直接进行数据识别。为什么网络字节序采用的是大端?而不是小端?
网络字节序采用的是大端,而主机字节序一般采用的是小端,那为什么网络字节序不采用小端呢?如果网络字节序采用小端的话,发送端和接收端在发生和接收数据时就不用进行大小端的转换了。
该问题有很多不同说法,下面列举了两种说法:
说法一: TCP在Unix时代就有了,以前Unix机器都是大端机,因此网络字节序也就采用的是大端,但之后人们发现用小端能简化硬件设计,所以现在主流的都是小端机,但协议已经不好改了。
说法二: 大端序更符合现代人的读写习惯。
为使网络程序具有可移植性,使同样的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表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如hton
l表示将32位长整数从主机字节序转换为网络字节序。
二、socket编程接口
2.1 socket常见API
// 创建 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);
以上接口是一层抽象的网络编程接口,适用于各种底层网络协议,如ipv4、ipv6以及后面的UNIX Domain Socket,然而,各种网络协议的地址格式并不相同。那么我们如何把不同地址格式的地址变为统一的地址格式交给以上API呢?引入了sockaddr
结构,
2.2 sockaddr结构
套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字),而很明显本地的进程间通信是不需要IP和PORT的,因此提供了sockaddr_in
结构体(ipv6—sockaddr_in6)和sockaddr_un
结构体,sockaddr_in
用于网络通信,sockaddr_un
用于本地通信。
为了统一地质结构的表示方法,于是就出现了sockeaddr
结构体,它用于统一地址结构的表示方法,使得不同的地址结构可以被bind()
、connect()
、recvfrom()
、sendto()
等函数调用。
该结构体与sockaddr_in
和sockaddr_un
的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族。
此时当我们在传参时,就不用传入sockeaddr_in
或sockeaddr_un
这样的结构体,而统一传入sockeaddr
这样的结构体从而实现了统一的API接口。
在这些API内部就可以提取sockeaddr
结构头部的16位进行识别,然后执行对应的操作。此时我们就通过通用sockaddr
结构,将参数类型进行了统一。
其实这种设计模式就是早期的多态。
三、简单的UDP网络程序
首先说明下我们的编程思路,首先肯定需要创建一个服务端对象,并初始化这个服务端开启服务。
3.1创建UDP套接字
那么对于初始化服务端,首先要做的一定是创建socket套接字。
int socket(int domain, int type, int protocol);
参数说明:
domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于struct sockaddr
结构的前16个位。如果是本地通信就设置为AF_UNIX
,如果是网络通信就设置为AF_INET
(IPv4)或AF_INET6
(IPv6)。
type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM
和SOCK_DGRAM
,如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM
,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM
,叫做流式套接字,提供的是流式服务。
protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。
返回值说明:
套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置。socket
函数底层做了什么?
当我们调用socket
函数创建套接字时,实际相当于我们打开了一个“网络文件”,打开后在内核层面上就形成了一个对应的struct file
结构体,同时该结构体被连入到了该进程对应的文件双链表,并将该结构体的首地址填入到了fd_array
数组当中下标为3的位置,此时fd_array
数组中下标为3的指针就指向了这个打开的“网络文件”,最后3号文件描述符作为socket
函数的返回值返回给了用户。
其中每一个struct file
结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等。其中文件对应的属性在内核当中是由struct inode
结构体来维护的,而文件对应的操作方法实际就是一堆的函数指针(比如read*
和write*
)在内核当中就是由struct file_operations
结构体(方法集)来维护的。
而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的“网络文件”来说,这里的文件缓冲区对应的就是网卡。
对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作.
而对于现在socket
函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。
3.2服务端绑定
现在套接字已经创建好了,我们还没有将这个套接字与网络进行绑定,即通信方式、IP和PORT等等都是未知的,而这些内容都存放在sockaddr
结构体中,所以我们需要利用bind
函数将socket
与sockaddr
进行绑定,即改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法,此时读数据和写数据对应的操作对象就是网卡了,所以绑定实际上就是将文件和网络关联起来。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。addrlen:传入的addr结构体的长度。返回值说明:
绑定成功返回0,绑定失败返回-1,同时错误码会被设置。这里我们采用的是网络通信,所以我们需要传入sockaddr_in
结构体的地址,注意强转为sockaddr*
类型。
struct sockaddr_in结构体
成员:
sin_family:表示协议家族。sin_port:表示端口号,是一个16位的整数。sin_addr:表示IP地址,是一个32位的整数。其中sin_addr
的类型是struct in_addr
,实际该结构体当中就只有一个成员s_addr
,该成员就是一个32位的整数,IP地址实际就是存储在这个整数当中的。
套接字创建完毕后我们就需要进行绑定了,但在绑定之前我们需要先定义一个struct sockaddr_in
结构,将对应的网络属性信息填充到该结构当中。由于该结构体当中还有部分选填字段,因此我们最好在填充之前对该结构体变量里面的内容进行清空,然后再将协议家族、端口号、IP地址等信息填充到该结构体变量当中。
需要注意的是,在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此我们需要使用前面说到的htons
函数将端口号转为网络序列。此外,由于网络当中传输的是整数IP,我们需要调用inet_addr
函数将字符串IP转换成整数IP,然后再将转换后的整数IP进行设置。
当网络属性信息填充完毕后,由于bind
函数提供的是通用参数类型,因此在传入结构体地址时还需要将struct sockaddr_in*
强转为struct sockaddr*
类型后再进行传入。
UdpServer(uint16_t port) : _sockfd(sockfddefault), _port(port), _ip(ip), _isrunning(false) {}void InitServer(){ // 1.创建UDP套接字 _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 返回文件描述符 if (_sockfd < 0) { LOG(FATAL, "socket error,%s,%d", strerror(errno), errno); exit(SOCKET_ERROR); } LOG(INFO, "socket create success,sockfd:%d", _sockfd); // 2.0填充sockaddr_in结构 struct sockaddr_in local; // struct sockaddr_in 系统提供的数据类型。local是变量,用户栈上开辟空间。 bzero(&local, sizeof(local)); // 将从&local开始的sizeof(local)大小的内存区域置零 local.sin_family = AF_INET; // 设置网络通信方式 local.sin_port = htons(_port); // port要经过网络传输给对面,所有需要从主机序列转换为网络序列 // a. 字符串风格的点分十进制的IP地址转成 4 字节IP // b. 主机序列,转成网络序列 // in_addr_t inet_addr(const char *cp) -> 该函数可以同时完成 a & b local.sin_addr.s_addr = inet_addr(_ip.c_str()); // "192.168.3.1" -> 字符串风格的点分十进制的IP地址 -> 4字节IP // 2.1bind绑定sockfd和网络信息(IP+PORT) int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); if (n < 0) { LOG(FATAL, "bind error,%s,%d", strerror(errno), errno); exit(BIND_ERROR); } LOG(INFO, "socket bind success");}
服务端绑定一般不指定IP,为什么?
当一个服务器的带宽足够大时,一台机器接收数据的能力就约束了这台机器的IO效率,因此一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个IP地址,但一台服务器上端口号为8081的服务只有一个。这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,如果这些数据也都想访问端口号为8081的服务。
**此时如果服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据。**而如果服务端绑定的是INADDR_ANY
(宏,值为0,表示任意IP),那么只要是发送给端口号为8081的服务的数据,系统都会可以将数据自底向上交给该服务端。
所以这里我们对代码做修改:
UdpServer(uint16_t port) : _sockfd(sockfddefault), _port(port), _isrunning(false) {}void InitServer(){ // 1.创建UDP套接字 _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 返回文件描述符 if (_sockfd < 0) { LOG(FATAL, "socket error,%s,%d", strerror(errno), errno); exit(SOCKET_ERROR); } LOG(INFO, "socket create success,sockfd:%d", _sockfd); // 2.0填充sockaddr_in结构 struct sockaddr_in local; // struct sockaddr_in 系统提供的数据类型。local是变量,用户栈上开辟空间。 bzero(&local, sizeof(local)); // 将从&local开始的sizeof(local)大小的内存区域置零 local.sin_family = AF_INET; // 设置网络通信方式 local.sin_port = htons(_port); // port要经过网络传输给对面,所有需要从主机序列转换为网络序列 // a. 字符串风格的点分十进制的IP地址转成 4 字节IP // b. 主机序列,转成网络序列 // in_addr_t inet_addr(const char *cp) -> 该函数可以同时完成 a & b // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // "192.168.3.1" -> 字符串风格的点分十进制的IP地址 -> 4字节IP local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY宏的值为0,给local.sin_addr.s_addr设置为0代表任意IP,因为一个服务器有多个IP,为了确保所有请求_port端口的请求都能得到相应,所以设置为0 // 2.1bind绑定sockfd和网络信息(IP+PORT) int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); if (n < 0) { LOG(FATAL, "bind error,%s,%d", strerror(errno), errno); exit(BIND_ERROR); } LOG(INFO, "socket bind success");}
字符串IP & 整数IP
IP地址的表现形式有两种:
字符串IP:类似于192.168.233.123
这种字符串形式的IP地址,叫做基于字符串的点分十进制IP地址,这种ip是给人看的。整数IP:IP地址在进行网络传输时所用的形式,用一个32位的整数来表示IP地址,这种ip是网络传输用的。 为什么要分两种IP表现形式?
如果我们在网络传输时直接以基于字符串的点分十进制IP的形式进行IP地址的传送,那么此时一个IP地址至少就需要15个字节,但实际并不需要耗费这么多字节。
点分十进制IP地址实际可以划分为四个区域,其中每一个区域的取值都是0~255,而这个范围的数字只需要用8个比特位就能表示,因此我们实际只需要32个比特位就能够表示一个IP地址。其中这个32位的整数的每一个字节对应的就是IP地址中的某个区域,我们将IP地址的这种表示方法称之为整数IP,此时表示一个IP地址只需要4个字节。
所以在网络编程中会涉及到字符串IP与整数IP之间的转换,而系统也提供给了我们转换的函数。
字符串IP转换为整数IP:
in_addr_t inet_addr(const char *cp);
该函数使用起来非常简单,我们只需传入待转换的字符串IP,该函数返回的就是转换后的整数IP。除此之外,inet_aton
函数也可以将字符串IP转换成整数IP,不过该函数使用起来没有inet_addr
简单。
整数IP转化为字符串IP:
char *inet_ntoa(struct in_addr in);
需要注意的是,传入inet_ntoa函数的参数类型是in_addr
,因此我们在传参时不需要选中in_addr
结构当中的32位的成员(即s_addr
)传入,直接传入in_addr
结构体即可。
3.3运行
以上创建套接字和绑定的操作都是属于初始化服务端的内容,那么接下来我们就需要编写服务端运行过程的代码,让服务端启动服务了。
服务器实际上就是在周而复始的为我们提供某种服务,服务器之所以称为服务器,是因为服务器运行起来后就永远不会退出,因此服务器实际执行的是一个死循环代码。
由于UDP服务器是不面向连接的,因此只要UDP服务器启动后,就可以直接读取客户端发来的数据。
接收数据的函数:
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:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:调用时传入期望读取的src_addr
结构体的长度,返回时代表实际读取到的src_addr
结构体的长度,这是一个输入输出型参数。
返回值说明:
读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。注意:
由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等。在调用recvfrom
读取数据时,必须将addrlen
设置为你要读取的结构体对应的大小。由于recvfrom
函数提供的参数也是struct sockaddr
*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*
类型进行强转。 发送数据的函数:
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:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。 addrlen:传入dest_addr结构体的长度。
返回值说明:
写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。注意:
由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。由于sendto
函数提供的参数也是struct sockaddr*
类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*
类型进行强转。 3.4简易echo服务器实现
以上主要是为了让大家认识以下网络编程的接口,那么接下来用一个例子带大家初步了解网络编程的思路。
下面我们实现一个简易echo服务器,他的功能就是客户端向服务端发送什么数据,服务端再将数据发送回来。
也就是说服务端需要接收客户端发送的数据recvfrom
,然后还需要将数据发送出去sendto
。
启动服务端服务
void Start(){ // 一直运行,直到管理者不想运行了, 服务器都是死循环 // UDP是面向数据报的协议 _isrunning = true; while (true) { char buffer[1024]; struct sockaddr_in peer; socklen_t len = sizeof(peer); // 必须初始化为sizeof(peer),不能是0 // 1.要先让server接收数据 ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); if (n > 0) { buffer[n] = 0; InetAddr addr(peer); LOG(DEBUG, "get message from [%s:%d]: %s\n", addr.Ip().c_str(), addr.Port(), buffer); // 2. 我们要将server收到的数据,发回给对方 sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len); } } _isrunning = false;}
InetAddr类的实现
我们想要将IP和PORT输出到屏幕上,这就必须进行一些转换工作,比如整数IP到点分十进制的IP转换,网络字节序到主机字节序的转换等,所以我们可以实现一个类,让类内部帮我们进行转换。
#pragma once#include <iostream>#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>// 这是一个可以获取点分十进制格式IP地址和Port端口号的类class InetAddr{ private: struct sockaddr_in _addr; std::string _ip; uint16_t _port; private: void GetAddress(std::string *ip, uint16_t *port) { *port = ntohs(_addr.sin_port); *ip = inet_ntoa(_addr.sin_addr); // inet_ntoa是一个用于将网络字节序的 IP 地址转换为点分十进制的字符串格式(如 "192.168.1.1")的函数 } public: InetAddr(const struct sockaddr_in &addr) : _addr(addr) { GetAddress(&_ip, &_port); } std::string Ip() { return _ip; } uint16_t Port() { return _port; } ~InetAddr() {}};
客户端程序的编写
客户端也需要进行类似服务端的初始化工作,即套接字的创建,绑定等。
#include <iostream>#include <string>#include <cstring>#include <cstdlib>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>void Usage(std::string proc){ std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;}// ./udpclient serverip serverportint main(int argc, char *argv[]){ if (argc != 3) { Usage(argv[0]); exit(1); } std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); // 1.创建socket int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { std::cerr << "socket error" << std::endl; } // 构建目标主机的socket信息 struct sockaddr_in server; memset(&server, 0, sizeof(server)); // bzero server.sin_family = AF_INET; server.sin_port = htons(serverport); server.sin_addr.s_addr = inet_addr(serverip.c_str()); // inet_addr用于将点分十进制的 IPv4 地址字符串转换成一个长整型数(通常是 u_long 或 in_addr_t 类型)。 // 客户端要不要bind? std::string message; // 2.直接通信即可(Start) while (true) { std::cout << "Please Enter# "; std::getline(std::cin, message); sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server)); 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::cout << "server echo# " << buffer << std::endl; } } return 0;}
客户端要不要绑定?
答案是肯定的,因为网络通信的前提就是需要客户端的IP和PORT,服务端的IP和PORT,通过他们两个网络中的进程才可以进行通信。但是客户端不能像服务端一样显式的bind,设想一个场景,淘宝写了一个客户端,显示绑定了端口号8080,而微信写的客户端也显示绑定的8080端口号,那此时就会因为端口冲突导致你只能使用一项服务,这很明显是不现实的,所以客户端绑定端口的操作由操作系统自动完成,就是为了防止客户端端口号冲突,一般在首次发送数据的时候绑定。
我们已经实现好了服务端的类,和客户端程序,接下来我们只需要再实现一个程序,调用服务端对象的初始化和启动方法:
#include <iostream>#include <memory>#include "UdpServer.hpp"void Usage(std::string proc){ std::cout << "Usage:\n\t" << proc << " local_port\n" << std::endl;}// ./udpserver port// 云服务器的port默认都是禁止访问的。云服务器放开端口8080 ~ 8085int main(int argc, char *argv[]){ if (argc != 2) { Usage(argv[0]); exit(USAGE_ERROR); } EnableScreen(); uint16_t port = std::stoi(argv[1]); std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port); // C++14 usvr->InitServer(); usvr->Start(); return 0;}
本地测试
首先利用127.0.0.1(回环地址),进行本地测试。
127.0.0.1
是一个特殊的IPv4地址,被称为“回环地址”或“localhost”。它通常用于指代本地计算机上的网络服务,而不是网络上的另一台计算机。在开发或测试阶段,开发人员经常需要在本地计算机上运行多个服务实例,并使用127.0.0.1
来访问它们。
网络测试
在网络测试前,你需要确保你的云服务器安全组配置已经打开了你所希望绑定的端口号,就像这样:
或者通过命令行的方式添加开放端口规则和重新加载:
sudo ufw allow xx/udpsudo ufw reload
我们可以利用netstat查看网络信息:
我们发现绑定的IP为0.0.0.0即任意IP,端口号8888,链接方式UDP。
netstat的命令行参数:
-n:number的意思,即IP和端口号都用数字的形式展示。-p:显示哪个进程或程序正在使用套接字(socket)。-u:仅显示 UDP 连接。-a:显示所有活动的网络连接和监听的服务器套接字。请注意,由于它显示了进程信息,因此你可能需要具有适当的权限才能运行它。在某些系统上,你可能需要使用 sudo
来运行此命令,如 sudo netstat -npua
。
青年人珍重的描写罢,时间正翻着书页,请你着笔! —青年人
实例,并使用127.0.0.1
来访问它们。
网络测试
在网络测试前,你需要确保你的云服务器安全组配置已经打开了你所希望绑定的端口号,就像这样:
[外链图片转存中…(img-P0BkEsE1-1720966085373)]
或者通过命令行的方式添加开放端口规则和重新加载:
sudo ufw allow xx/udpsudo ufw reload
[外链图片转存中…(img-s8ieJtHZ-1720966085374)]
我们可以利用netstat查看网络信息:
[外链图片转存中…(img-xXMCimr7-1720966085374)]
我们发现绑定的IP为0.0.0.0即任意IP,端口号8888,链接方式UDP。
netstat的命令行参数:
-n:number的意思,即IP和端口号都用数字的形式展示。-p:显示哪个进程或程序正在使用套接字(socket)。-u:仅显示 UDP 连接。-a:显示所有活动的网络连接和监听的服务器套接字。请注意,由于它显示了进程信息,因此你可能需要具有适当的权限才能运行它。在某些系统上,你可能需要使用 sudo
来运行此命令,如 sudo netstat -npua
。
青年人珍重的描写罢,时间正翻着书页,请你着笔! —青年人