主页 > 游戏开发  > 

【网络编程】之TCP通信步骤

【网络编程】之TCP通信步骤

【网络编程之TCP通信步骤】 TCP网络通信TCP网络通信的步骤对于服务器端对于客户端 TCP实现echo功能代码实现服务器端getsockname函数介绍 客户端效果展示 对比两组函数

TCP网络通信 TCP网络通信的步骤 对于服务器端

创建监听套接字。(调用socket函数)

使用 socket 函数创建一个 TCP 套接字,为服务器提供网络通信的基础。该套接字将用于监听客户端的连接请求。例如: int server_fd = socket(AF_INET, SOCK_STREAM, 0);

显式bind服务器的IP地址和端口号。

使用 bind 函数将服务器的 IP 地址和端口号绑定到创建的套接字上。确保服务器能够通过指定的地址和端口来接受客户端的连接请求。例如: struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用网络接口 server_addr.sin_port = htons(8080); // 指定端口号 bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

设置监听套接字为监听状态,监听客户端请求。

使用 listen 函数将套接字转换为监听状态,服务器开始等待客户端的连接请求。 listen(server_fd, 5); // 最大监听队列长度为 5

接受客户端的连接请求。(同时会创建一个专门与这个客户端通信的套接字)

使用 accept 函数接受客户端的连接请求。在此期间,服务器会阻塞,直到有客户端发起连接请求。接受客户端请求后,函数会返回一个新的套接字,该套接字专门用于与客户端通信。 int client_fd = accept(server_fd, NULL, NULL);

最后两个参数是与正在请求连接的客户端地址相关的参数,如果你不需要发送数据,可以都传NULL。

返回的套接字(即与客户端通信的套接字)在大多数情况下会继承 监听套接字 的 本地地址和端口。也就是说它并不需要重复bind。

收发数据:

服务器通过 recv 接收客户端发送的数据,并通过 send 向客户端发送响应数据。此时,通信已经通过与客户端建立的专用套接字进行。

recv(client_fd, buffer, sizeof(buffer), 0); // 接收数据 send(client_fd, response, strlen(response), 0); // 发送数据

细节:

listen函数的作用是将监听套接字设置为监听状态,并不会阻塞,当监听套接字设置为监听状态后,服务器端才可以监听客户端请求,进而建立连接。当客户端向服务器发起连接请求时,这些请求不会直接被服务器立即处理,而是由操作系统暂时存放在监听队列中。监听队列的长度backlog,由用户指定。但是它只影响监听队列的大小,而不限制已经成功建立的连接数量。在默认情况下,accept 函数会阻塞,直到有客户端的连接请求到来并完成三次握手。 对于客户端

和服务器端的行为类似,不同的是:

客户端是请求连接方,网络中不会有进程与它主动建立连接,所以它不需要监听套接字,进而也不需要调用listen函数。它需要主动调用connect函数与服务器端发起连接请求。

步骤:

创建通信的套接字。bind(不用显式bind,当客户端发起连接时,OS会自动bind)。向服务器发起连接请求(connect函数)。收发数据。关闭通信套接字。 TCP实现echo功能

客户端发送什么,服务器就返回什么。

代码实现 服务器端

服务器端需要不停的建立连接,可以使用多线程、线程池、或者多进程来实现。但是不能使用一个单线程的进程,因为可能需要连接的客户端有很多,建立连接成功后,每个连接都会进入死循环(不停收发数据),直到客户端退出。

如果使用单线程的进程,一个客户端建立连接成功,它就会阻塞到该客户端处理数据的函数中,无法继续处理请求了。

我们使用线程池版本来实现服务器端的代码:

#pragma once #include<unistd.h> #include<sys/socket.h> #include<arpa/inet.h> #include<netinet/in.h> #include<sys/types.h> #include<cstdio> #include<cstdlib> #include"Log.hpp" #include"InetAddr.hpp" #include"ThreadPool.hpp" // 错误码 enum { SOCKETERROR = 1, BINDERROR, USAGEERROR }; // 定义funccommunicate为一个函数类型,用于线程池的任务队列 using funccommunicate = function<void()>; // TcpServer类实现TCP服务器的功能 class TcpServer { private: int _listensock; // 监听套接字 uint16_t _port; // 服务器端口 bool _is_running; // 服务器是否在运行 public: // 构造函数,初始化套接字和端口号 TcpServer(uint16_t port):_listensock(-1),_port(port),_is_running(false) {} // 初始化服务器,创建监听套接字并绑定地址和端口 void InitServer() { _listensock = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字 if (_listensock == -1) { LOG(FATAL, "socket error"); exit(1); } LOG(INFO, "socket success"); // 配置服务器地址 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(_port); // 转换端口号为网络字节序 addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有可用接口 // 绑定套接字到指定端口和IP地址 if (bind(_listensock, (struct sockaddr*)&addr, sizeof(addr)) == -1) { LOG(FATAL, "bind error"); exit(1); } LOG(INFO, "bind success"); // 开始监听客户端连接,最大连接数为5 if (listen(_listensock, 5) == -1) { LOG(FATAL, "listen error"); exit(1); } } // 处理每个连接的业务逻辑 void Service(int sockfd, InetAddr addr) { LOG(INFO, "new connect: %s:%d", inet_ntoa(addr.addr().sin_addr), ntohs(addr.addr().sin_port)); // 输出连接信息 // 处理接收和响应循环 while (true) { char buffer[1024]; // 用于接收数据的缓冲区 memset(buffer, 0, sizeof(buffer)); // 初始化缓冲区为0 int n = recv(sockfd, buffer, sizeof(buffer), 0); // 接收客户端数据 // 构建客户端信息字符串 string sender = "[" + addr.ip() + ":" + to_string(addr.port()) + "]#"; if (n == -1) // 如果接收失败 { perror("recv"); // 输出错误信息 break; } else if (n == 0) // 客户端关闭了连接 { LOG(INFO, "client close"); break; } else // 数据接收成功 { buffer[n] = 0; // 确保接收的数据是一个合法的C字符串 LOG(INFO, "%s%s", sender.c_str(), buffer); // 打印接收到的数据 string echoserver = "[echo server]#" + string(buffer); // 构建回显信息 // 获取服务器端新套接字的本地地址和端口 struct sockaddr_in local_addr; socklen_t len = sizeof(local_addr); if (getsockname(sockfd, (struct sockaddr*)&local_addr, &len) == -1) { perror("Getsockname failed"); return; } // 输出本地地址和端口信息 printf("New socket local address: %s:%d\n", inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port)); // 发送回显消息到客户端 send(sockfd, echoserver.c_str(), echoserver.size(), 0); } } close(sockfd); // 关闭套接字,结束与客户端的通信 }; // 服务器主循环,不断接收新的连接请求 void Loop() { _is_running = true; while (_is_running) { struct sockaddr_in peer; // 存储客户端的地址信息 socklen_t len = sizeof(peer); // 等待并接受新的连接请求 int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len); cout << "建立新连接成功" << endl; if (sockfd == -1) // 如果接收连接失败,输出错误信息 { perror("accept"); break; } InetAddr addr(peer); // 将客户端地址封装到InetAddr对象中 // 版本1:直接调用Service处理连接(不建议这种方式,因为它会阻塞并限制并发) // Service(sockfd, addr); // 每次只能处理一个连接,无法同时处理多个连接 // 版本2:使用线程池处理连接(推荐的方式,支持并发) bool ret = ThreadPoolModule::ThreadPool<funccommunicate>::GetInstance()->EnqueueTask(bind(&TcpServer::Service, this, sockfd, addr)); } _is_running = false; } // 析构函数,关闭监听套接字 ~TcpServer() { if (_listensock != -1) { close(_listensock); } } }; getsockname函数介绍 int getsockname(int sockfd, struct sockaddr *restrict addr,socklen_t *restrict addrlen); 函数功能:返回当前套接字bind的地址。参数: int sockfd:要查看bind地址的套接字描述符。struct sockaddr *restrict addr:输出型参数,该函数会把地址写进这个变量指向的空间中。socklen_t *restrict addrlen:指向保存结构体大小变量的指针,输入型参数。 返回值:成功0被返回。否则-1被返回,errno被设置。头文件: <sys/socket.h>。 客户端

TcpClient.cc:

#include <iostream> #include <string> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> using namespace std; // Usage函数:如果程序参数不正确,打印如何使用该程序的提示信息 void Usage(char* s) { cout << "Usage:\n\t" << s << " serverip serverport" << endl; exit(1); } int main(int argc, char* argv[]) { // 检查传入的参数数量,若参数不正确,则调用Usage函数 if(argc != 3) { Usage(argv[0]); // 打印使用帮助信息并退出 return 1; } // 从命令行参数获取服务器IP地址和端口号 string ip = argv[1]; // 服务器IP地址 uint16_t port = stoi(argv[2]); // 服务器端口号,将字符串转换为整数 // 创建套接字,使用IPv4地址族和TCP协议(SOCK_STREAM表示流式套接字) int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字 if(sockfd == -1) // 如果创建套接字失败,打印错误并返回 { perror("socket create error"); // 输出错误信息 return 1; } // 客户端不需要bind,bind通常用于服务器端 // 客户端也不需要listen,监听请求是服务器端的工作 // 设置服务器的地址信息 struct sockaddr_in addr; // sockaddr_in结构体用于存储服务器的网络地址 addr.sin_family = AF_INET; // 使用IPv4地址族 addr.sin_port = htons(port); // 设置服务器的端口号(htons将端口号转换为网络字节序) inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr); // 将IP地址字符串转换为网络字节序的二进制格式 // 连接到服务器 if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) // 调用connect连接服务器 { perror("connect error"); // 如果连接失败,输出错误信息 return 1; } // 客户端和服务器之间进行通信 while(true) { string message; cout << "please input message:"; // 提示用户输入消息 getline(cin, message); // 从标准输入获取一行字符串作为消息 // 将输入的消息发送到服务器 send(sockfd, message.c_str(), message.size() + 1, 0); // 发送消息到服务器,+1用于包括消息结尾的'\0' // 接收服务器返回的消息 char buffer[1024]; // 定义接收缓冲区,大小为1024字节 memset(buffer, 0, sizeof(buffer)); // 将缓冲区初始化为0 int n = recv(sockfd, buffer, sizeof(buffer), 0); // 从服务器接收数据 if(n == -1) // 如果接收数据失败 { perror("recv error"); // 输出错误信息 break; // 跳出循环,关闭连接 } else if(n == 0) // 如果服务器关闭了连接 { cout << "server close" << endl; // 打印提示信息 break; // 跳出循环,结束通信 } else // 数据接收成功 { buffer[n] = 0; // 确保接收到的数据是一个合法的C字符串(添加终止符'\0') cout << buffer << endl; // 输出服务器返回的消息 } } // 关闭套接字,结束与服务器的通信 close(sockfd); // 关闭套接字 return 0; // 程序正常结束 } 效果展示


打印服务器端与客户端通信的socket套接字描述符的地址,发现端口一样(8080),但是和虚拟机客户端和本地的客户端通信的服务器端的套接字bind的IP地址不同,这是因为服务器bind的IP地址是0.0.0.0,表示监听主机内所以网络接口的流量,虚拟机客户端访问和本地访问的流量进入主机内,流量会经过不同的网络接口,所以与他们通信的套接字的IP地址会不同。 对比两组函数

recv、recvform、read。

recv与read:

相似之处:都是从文件描述符🀄️读取数据。

不同之处:recv是专门用于网络套接字中读取数据,而read更加通用,可以读取任何类型的文件描述符。允许指定标志(flags)来控制接收操作的行为。例如,标志可以指定如何处理数据或是否采用非阻塞模式等。

recvfrom与recv:

recvfrom:recvfrom() 是设计用来接收数据包并且能够获取发送方的地址信息的。常常在UDP中使用,在TCP中也可以接收数据,但它不会返回对端的地址信息。

recv:recv是专门用于TCP接收数据,它是从一个已经建立的连接中获取数据,因此不需要提供发送方的地址信息。recv也能在UDP中接收数据,但它无法获取发送方的地址信息。

send、sendto、write:

相同点:这一组函数都是用于发送数据的。

不同点:send与sendto用于网络通信,从套接字描述符中读取数据。而write更加的通用。send用于TCP通信,面向连接,不需要指定客户端地址。而sendto需要指定客户端的地址。

尽管从用户角度看它们的功能略有重叠,网络相关的功能通常会选择 send 和 sendto,因为它们支持更多与网络协议相关的选项。
标签:

【网络编程】之TCP通信步骤由讯客互联游戏开发栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【网络编程】之TCP通信步骤