Ⅱ、运行服务器
当服务器初始化完毕后我们就可以启动服务器了,由于服务器是一个永不退出的进程,所以服务器运行以后一定是一个死循环!
读取数据
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
,同时错误码会被设置。
发送数据
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,同时错误码会被设置。
现在服务端通过recvfrom
函数读取客户端数据,我们可以先将读取到的数据当作字符串看待,将读取到的数据的最后一个位置设置为’\0’,此时我们就可以将读取到的数据进行输出,同时我们也可以将获取到的客户端的IP地址和端口号也一并进行输出。
需要注意的是,我们获取到的客户端的端口号此时是网络序列,我们需要调用ntohs
函数将其转为主机序列再进行打印输出。同时,我们获取到的客户端的IP地址是整数IP,我们需要通过调用inet_ntoa
函数将其转为字符串IP再进行打印输出。
// udp_server.hpp #pragma once #include <iostream> #include <string> #include <cstring> #include <cerrno> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> enum { SOCKET_ERR = 1, BIND_ERR, USAGE_ERR }; class UdpServer { public: UdpServer(uint16_t port) :_port(port) { std::cout << "port : " << _port << std::endl; } void UdpServerInit() { // 1. 创建套接字 _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0) { std::cerr << "create socket fail : " << strerror(errno) << std::endl; exit(SOCKET_ERR); } std::cout << "create socket success! " << "sockfd : " << _sockfd << std::endl; // 2. 填充sockaddr_in结构体 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; // 将主机序列转换为网络序列 local.sin_addr.s_addr = INADDR_ANY; local.sin_port = htons(_port); // 3. 绑定IP,端口号 if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) != 0) { std::cerr << "bind fail :" << strerror(errno) << std::endl; exit(BIND_ERR); } std::cout << "bind success !" << std::endl; } void UdpServerStart() { // 缓冲区 char buf[2048]; // 网络信息结构体 struct sockaddr_in peer; socklen_t len = sizeof(peer); // 死循环不能让服务器退出 while (true) { memset(&peer, 0, len); // 收取消息 ssize_t num = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer, &len); if (num < 0) { std::cerr << "recvfrom fail !" << std::endl; continue; } else { // 结尾补上\0,形成C风格字符串 buf[num] = '\0'; } // 提取客户端的ip和端口号 std::string peer_ip = inet_ntoa(peer.sin_addr); uint16_t peer_port = ntohs(peer.sin_port); std::cout << peer_ip << " | " << peer_port << " |# " << buf << std::endl; // 发消息 sendto(_sockfd, buf, strlen(buf), 0, (struct sockaddr*)&peer, len); } } ~UdpServer() { if (_sockfd > 0) { close(_sockfd); } } private: int _sockfd; // 套接字的文件描述符 uint16_t _port; // 端口号 };
我们服务器启动的时候需要指定端口号,所以这里使用了命令行参数。
// udp_server.cpp #include "udp_server.hpp" #include <iostream> #include <memory> // 使用手册 static void usage(std::string proc) { std::cout << "usage\n\t" << proc << " 端口号" << std::endl; } // 命令行参数,必须输入两个参数,一个是程序名,一个是端口号 int main(int argc, char* argv[]) { if (argc != 2) { usage(argv[0]); exit(USAGE_ERR); } // 提取本地端口号 uint16_t port = atoi(argv[1]); std::unique_ptr<UdpServer> up(new UdpServer(port)); up->UdpServerInit(); up->UdpServerStart(); return 0; }
程序启动以后我们可以使用netstat -naup
显示进程的网络信息。
netstat
常用选项说明:
- -n:直接使用IP地址,而不通过域名服务器。
- -a: 显示所有连接中的接口信息。
- -t:显示TCP传输协议的连线状况。
- -u:显示UDP传输协议的连线状况。
- -p:显示正在使用Socket的程序识别码和程序名称。
运行结果
查看网络信息
netstat
命令显示的信息中:
- Proto表示协议的类型
- Recv-Q表示网络接收队列
- Send-Q表示网络发送队列
- Local Address表示本地地址,
- Foreign Address表示外部地址
- State表示当前的状态
- PID表示该进程的进程ID
- Program name表示该进程的程序名称。
其中Foreign Address写成0.0.0.0:*表示任意IP地址、任意的端口号的程序都可以访问当前进程。
Ⅲ、关于客户端的绑定问题
首先,由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号,只不过服务端需要显示的进行IP和端口号的绑定,而客户端不需要显示的进行绑定的,这个绑定的工作由操作系统来进行绑定,当我们调用类似于sendto
这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号。
服务器是为了给客户提供服务的,因此服务器必须要让客户知道自己的IP地址和端口号,否则客户端是无法向服务端发起请求的,这就是服务端要进行显示绑定的原因,只有一个进程绑定了端口号之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。
而客户端在通信时虽然也需要端口号,但客户端一般是不进行绑定的,客户端访问服务端的时候,端口号只要是唯一的就行了,不需要明确是那个特定的端口号。
一台设备上可以运行很多客户端,例如:B站客户端绑定了8080端口号,那么以后8080端口号就只能给B站客户端使用,如果8080端口号又被淘宝客户端绑定了并且淘宝先启动了,那么B站客户端就无法启动了,因此客户端端口通常是不绑定,由OS动态分配,也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要我们的端口号没有被耗尽,客户端就永远可以启动。
Ⅳ、启动客户端
客户端的编写与服务端类似,只不过客户端不需要我们进行绑定工作的,此外作为一个客户端,它必须知道它要访问的服务端的IP地址和端口号,因此在我们启动客户端时中需要引入服务端的IP地址和端口号。
客户端和服务端在功能上是相互补充的,我们上面的服务器是在读取客户端发来的数据然后回发回去,那么这里我们的客户端就应该向服务端发送数据,然后接收服务器回发的数据。
// client.cpp #include <iostream> #include <cstring> #include <cerrno> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> enum { SOCKET_ERR = 1, BIND_ERR, USAGE_ERR }; // 使用手册 static void usage(std::string proc) { std::cout << "usage\n\t" << proc << " IP 端口" << std::endl; } // 命令行参数,必须输入三个参数,一个是程序名,一个是IP,一个是端口号 int main(int argc, char* argv[]) { if (argc != 3) { usage(argv[0]); exit(USAGE_ERR); } // 1. 得到服务器的IP和端口 std::string server_ip = argv[1]; uint16_t server_port = atoi(argv[2]); // 2. 创建套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { std::cerr << "create socket fail : " << strerror(errno) << std::endl; exit(SOCKET_ERR); } // 3. 填充server结构体 struct sockaddr_in server; socklen_t len = sizeof(server); memset(&server, 0, len); server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr(server_ip.c_str()); server.sin_port = htons(server_port); // 4. 业务处理 std::string message; char buf[2048]; while (true) { std::cout << "[pan的服务器] :> "; getline(std::cin, message); // 发送消息 // 在我们首次调用系统调用发送数据时,OS会随机选择一个端口号 + 自己的IP进行bind ssize_t num = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len); if (num < 0) { std::cerr << "sendto fail !" << std::endl; continue; } struct sockaddr_in temp; socklen_t temp_len = sizeof(temp); memset(&temp, 0, temp_len); // 收消息 num = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&temp, &temp_len); if (num < 0) { std::cerr << "recvfrom fail !" << std::endl; continue; } else { buf[num] = '\0'; } std::cout << "server's message | " << buf << std::endl; } return 0; }
Ⅴ、本地测试
现在服务端和客户端的代码都已经编写完毕,我们可以先进行本地测试,现在我们运行服务器时指明端口号为8080,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1地址,服务端的端口号就是8080。
127.0.0.1
:本地环回,表示当前主机的地址,通常用来进行本地通信或测试。
我们要让服务端先运行,然后再让客户端运行,之后提示我们进行输入,当我们在客户端输入数据后,客户端将数据发送给服务端,此时服务端再将收到的数据打印输出后回发,这时我们在服务端和客户端的窗口都能看到我们输入的内容。
此时我们再用netstat
命令查看网络信息,可以看到服务端的端口是8080,客户端的端口是44777。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。
Ⅵ、网络测试
如果你是云服务器,请确保你想使用的端口已经开放,下面是腾讯云的云服务器开放端口的方法:
好了,我们开始进行网络测试:
你可以将此客户端软件给更多的人,让它们都能够连接你的服务器,进行网络通信。