做网站是用什么语言,线上推广员是干什么的,成都开发微信小程序,wordpress点击文章不能进入【网络编程】之Udp网络通信步骤 TCP网络通信TCP网络通信的步骤对于服务器端对于客户端 TCP实现echo功能代码实现服务器端getsockname函数介绍 客户端效果展示 对比两组函数 TCP网络通信
TCP网络通信的步骤
对于服务器端 创建监听套接字。#xff08;调用socket函数#xff… 【网络编程】之Udp网络通信步骤 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
#includeunistd.h
#includesys/socket.h
#includearpa/inet.h
#includenetinet/in.h
#includesys/types.h
#includecstdio
#includecstdlib
#includeLog.hpp
#includeInetAddr.hpp
#includeThreadPool.hpp// 错误码
enum
{SOCKETERROR 1,BINDERROR,USAGEERROR
};// 定义funccommunicate为一个函数类型用于线程池的任务队列
using funccommunicate functionvoid();// 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);// 开始监听客户端连接最大连接数为5if (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)); // 初始化缓冲区为0int 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::ThreadPoolfunccommunicate::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.husing 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;}// 客户端不需要bindbind通常用于服务器端// 客户端也不需要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)); // 将缓冲区初始化为0int 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字符串添加终止符\0cout 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 recvfromrecvfrom() 是设计用来接收数据包并且能够获取发送方的地址信息的。常常在UDP中使用在TCP中也可以接收数据但它不会返回对端的地址信息。 recvrecv是专门用于TCP接收数据它是从一个已经建立的连接中获取数据因此不需要提供发送方的地址信息。recv也能在UDP中接收数据但它无法获取发送方的地址信息。 send、sendto、write 相同点这一组函数都是用于发送数据的。 不同点send与sendto用于网络通信从套接字描述符中读取数据。而write更加的通用。send用于TCP通信面向连接不需要指定客户端地址。而sendto需要指定客户端的地址。 尽管从用户角度看它们的功能略有重叠网络相关的功能通常会选择 send 和 sendto因为它们支持更多与网络协议相关的选项。