当前位置: 首页 > news >正文

从入门到精通:网络编程套接字(万字详解,小白友好,建议收藏)

一、预备知识

1.1 理解源IP地址和目的IP地址

在网络编程中,IP地址(Internet Protocol Address)是每个连接到互联网的设备的唯一标识符。IP地址可以分为IPv4和IPv6两种类型。IPv4地址是由32位二进制数表示,通常分为四个八位组,以十进制数表示,例如192.168.0.1。IPv6地址是由128位二进制数表示,以16进制数表示,例如2001:0db8:85a3:0000:0000:8a2e:0370:7334。

在IP数据包的头部,有两个IP地址,分别是源IP地址和目的IP地址。源IP地址表示发送数据包的设备地址,而目的IP地址则是接收数据包的设备地址。IP地址的存在使得我们能够在庞大的网络中找到特定的设备,就像我们在现实世界中通过邮寄地址找到特定的人或公司一样。

然而,光有IP地址并不能完成通信。想象一下发送QQ消息的场景,有了IP地址,我们可以将消息发送到对方的设备上,但这还不足以确定消息应该由哪个程序来解析。这时我们就需要端口号的帮助。端口号和IP地址结合使用,可以唯一标识网络中的某个进程,从而确保数据包能够正确送达目标应用程序。

在现代网络通信中,IP地址的管理和分配由互联网名称与数字地址分配机构(ICANN)进行。IP地址的分配遵循一定的规则,以确保全球唯一性和合理使用。例如,私有IP地址(如192.168.x.x)仅用于局域网内部,不能在互联网上使用。公网IP地址则由ISP(Internet Service Provider)分配,用于互联网上的设备通信。

理解IP地址的基础知识是网络编程的第一步。在实际开发中,我们经常需要处理IP地址的转换、验证和解析等操作。Python提供了丰富的库函数来帮助我们完成这些任务,例如socket库和ipaddress库等。通过这些工具,我们可以方便地进行IP地址的处理和操作。

1.2 认识端口号

端口号是网络编程中另一个重要的概念。端口号是传输层协议的一部分,用于标识网络上的进程。它是一个16位的整数,范围从0到65535,每个端口号都唯一对应一个进程,从而告诉操作系统当前数据应交给哪个进程处理。

端口号的作用类似于现实生活中的房间号。如果IP地址是一个公寓楼的地址,那么端口号就是具体的房间号。通过IP地址我们可以找到公寓楼,而通过端口号我们可以找到具体的房间,进而找到住在这个房间的人(即进程)。

端口号分为三类:公认端口(0-1023)、注册端口(1024-49151)和动态/私有端口(49152-65535)。公认端口由IANA(Internet Assigned Numbers Authority)管理,通常用于系统服务和知名应用程序,例如HTTP使用端口80,HTTPS使用端口443。注册端口用于用户应用程序和服务,可以由用户自行申请和使用。动态/私有端口则用于临时或私有应用程序,不需要注册。

在网络编程中,我们常常需要绑定端口号,以便服务器能够接收客户端的请求。例如,在创建一个TCP服务器时,我们需要调用bind函数将套接字绑定到一个特定的IP地址和端口号上。这样,当客户端发送请求到这个IP地址和端口号时,服务器就能够接收到请求并进行处理。

需要注意的是,端口号的使用需要遵循一些基本的原则。首先,避免使用已知的公认端口,以免与系统服务发生冲突。其次,确保端口号唯一性,一个端口号只能被一个进程占用。最后,注意端口号的范围,避免使用保留端口和特殊用途端口。

1.3 理解源端口号和目的端口号

在传输层协议(TCP和UDP)的数据段中,有两个端口号,分别是源端口号和目的端口号。源端口号表示发送数据的进程的端口号,而目的端口号则表示接收数据的进程的端口号。这两个端口号描述了“数据是谁发的,要发给谁”。

例如,我们在发送快递时,会标明寄件人和收件人的信息。寄件人的信息相当于源端口号,而收件人的信息相当于目的端口号。通过源端口号和目的端口号的结合,数据包可以在网络中准确地传输到目标进程。

在TCP协议中,连接的建立和断开都涉及到源端口号和目的端口号。在三次握手过程中,客户端通过源端口号向服务器的目的端口号发送SYN请求,服务器通过目的端口号向客户端的源端口号发送SYN-ACK应答,客户端再通过源端口号向服务器的目的端口号发送ACK确认,从而建立连接。在四次挥手过程中,客户端和服务器通过源端口号和目的端口号的交互来完成连接的断开。

在UDP协议中,源端口号和目的端口号用于标识数据报的发送方和接收方。由于UDP是无连接协议,每个数据报都是独立的,源端口号和目的端口号在每个数据报中都是独立存在的,不同的数据报可以有不同的端口号。

源端口号和目的端口号的存在,使得网络通信更加灵活和高效。通过合理使用端口号,我们可以实现多种多样的网络应用和服务。例如,在一个网络应用中,我们可以使用多个端口号来处理不同类型的请求,如HTTP请求、文件传输请求和即时通讯请求等。

1.4 认识TCP协议

TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,提供可靠的数据传输。它通过三次握手建立连接,保证数据的完整性和顺序性,适用于对传输可靠性要求高的场景。

TCP协议的特点包括:

  • 面向连接:在传输数据之前,必须先建立连接。
  • 可靠传输:通过序号、确认、重传和超时等机制,保证数据的可靠传输。
  • 流量控制:通过滑动窗口机制,控制发送方发送数据的速度,避免接收方接收不过来。
  • 拥塞控制:通过拥塞窗口和慢启动机制,控制网络中的数据流量,避免网络拥塞。

在TCP协议中,数据被分成若干个数据段(Segment)进行传输。每个数据段都有一个序号,用于标识数据在整个数据流中的位置。接收方在接收到数据段后,会发送一个确认报文(ACK)给发送方,确认已接收到的数据段。发送方在接收到确认报文后,会继续发送后续的数据段。如果发送方在一定时间内没有收到确认报文,会进行数据重传,直到接收到确认报文为止。

TCP协议的连接建立过程称为三次握手:

  1. SYN:客户端向服务器发送一个SYN报文,表示请求建立连接。
  2. SYN-ACK:服务器收到SYN报文后,回应一个SYN-ACK报文,表示同意建立连接。
  3. ACK:客户端收到SYN-ACK报文后,回应一个ACK报文,表示连接建立完成。

TCP协议的连接断开过程称为四次挥手:

  1. FIN:客户端向服务器发送一个FIN报文,表示请求断开连接。
  2. ACK:服务器收到FIN报文后,回应一个ACK报文,表示同意断开连接。
  3. FIN:服务器向客户端发送一个FIN报文,表示准备断开连接。
  4. ACK:客户端收到FIN报文后,回应一个ACK报文,表示断开连接完成。

TCP协议在实际应用中有着广泛的应用,例如HTTP、FTP、SMTP等常见的网络协议都基于TCP协议实现。通过理解和掌握TCP协议,我们可以更好地进行网络编程,开发出高效、可靠的网络应用。

1.5 认识UDP协议

UDP(User Datagram Protocol,用户数据报协议)是无连接的协议,不保证数据的可靠传输。它以数据报的形式发送数据,每个数据报都是独立的,适用于实时性要求高但对可靠性要求低的场景,如视频直播、在线游戏等。

UDP协议的特点包括:

  • 无连接:在传输数据之前,不需要建立连接,每个数据报都是独立的。
  • 不可靠传输:UDP协议不保证数据报的传输成功,也不保证数据报的顺序,数据可能丢失、重复或乱序。
  • 面向数据报:UDP协议以数据报的形式发送和接收数据,每个数据报都有独立的报头和数据部分。

UDP协议的应用场景非常广泛,尤其在实时性要求高的场景中,UDP协议的无连接和低开销特点使得其非常适用。例如,在线游戏、视频会议和语音通话等应用通常采用UDP协议,以确保数据能够快速传输,尽可能减少延迟。

在UDP协议中,每个数据报都包含源端口号、目的端口号、长度和校验和等信息。源端口号和目的端口号用于标识数据报的发送方和接收方,长度表示数据报的长度,校验和用于检测数据报在传输过程中是否出现错误。由于UDP协议不提供重传和确认机制,数据报在传输过程中可能会丢失或出现错误,因此在应用层需要额外的处理逻辑来保证数据的完整性和可靠性。

1.6 网络字节序

在网络中,多字节数据的传输顺序有两种:大端字节序(Big-endian)和小端字节序(Little-endian)。大端字节序是指数据的高字节存储在内存的低地址,而小端字节序则是指数据的低字节存储在内存的低地址。不同的计算机体系结构可能采用不同的字节序,因此在进行网络传输时,需要进行字节序的转换,以保证数据在不同体系结构的计算机之间能够正确传输和解析。

TCP/IP协议规定网络数据流采用大端字节序,即低地址存储高字节。为了保证不同架构计算机之间的兼容性,我们需要进行字节序转换。C语言标准库提供了以下函数来进行字节序的转换:

  • htonl(Host to Network Long):将32位长整数从主机字节序转换为网络字节序。
  • htons(Host to Network Short):将16位短整数从主机字节序转换为网络字节序。
  • ntohl(Network to Host Long):将32位长整数从网络字节序转换为主机字节序。
  • ntohs(Network to Host Short):将16位短整数从网络字节序转换为主机字节序。

这些函数名中的h表示host(主机),n表示network(网络),l表示32位长整数,s表示16位短整数。例如,htonl表示将32位的长整数从主机字节序转换为网络字节序,用于在发送数据前对IP地址等数据进行转换。

通过字节序转换函数,我们可以确保网络程序具有良好的可移植性,使得同样的代码在大端和小端机器上都能正确运行。在实际开发中,正确处理字节序是保证网络通信成功的关键步骤。

二、socket编程接口

2.1 socket 常见API

socket API是网络编程的基础接口,适用于各种底层网络协议,如IPv4、IPv6等。以下是常见的socket API函数:

  • int socket(int domain, int type, int protocol);:创建一个新的套接字。domain参数指定协议族(如AF_INET用于IPv4),type参数指定套接字类型(如SOCK_STREAM用于TCP),protocol参数指定协议(通常为0,由系统自动选择)。
  • int bind(int socket, const struct sockaddr *address, socklen_t address_len);:将套接字绑定到本地地址。address参数指定本地地址和端口号,address_len参数指定地址的长度。
  • int listen(int socket, int backlog);:将套接字设置为监听模式,等待客户端连接。backlog参数指定等待连接队列的最大长度。
  • int accept(int socket, struct sockaddr *address, socklen_t *address_len);:接受客户端连接请求。address参数用于存储客户端的地址,address_len参数用于存储地址的长度。
  • int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);:与服务器建立连接。addr参数指定服务器的地址和端口号,addrlen参数指定地址的长度。

这些API函数构成了网络编程的基础,通过它们我们可以实现各种网络通信功能。例如,使用socket函数创建一个套接字,使用bind函数将其绑定到本地地址和端口号,使用listen函数将其设置为监听模式,使用accept函数接受客户端连接请求,使用connect函数与服务器建立连接。

在实际开发中,我们经常需要结合使用这些API函数来实现复杂的网络通信功能。例如,在实现一个TCP服务器时,我们可以使用socket函数创建一个服务器套接字,使用bind函数将其绑定到本地地址和端口号,使用listen函数将其设置为监听模式,然后使用accept函数接受客户端连接请求。在每个连接请求到来时,我们可以创建一个新的套接字用于与客户端通信,通过这个新的套接字接收和发送数据。

2.2 sockaddr结构

sockaddr结构是网络编程中通用的地址结构,不同的网络协议对应不同的地址结构,如IPv4地址用sockaddr_in表示,IPv6地址用sockaddr_in6表示。通过将特定类型的地址结构转换为sockaddr,可以实现代码的通用性。

sockaddr结构的定义如下:

struct sockaddr {sa_family_t sa_family;  // 地址族(如AF_INET用于IPv4)char sa_data[14];       // 地址数据(实际长度由地址族决定)
};

对于IPv4地址,通常使用sockaddr_in结构,该结构的定义如下:

struct sockaddr_in {sa_family_t sin_family;  // 地址族(AF_INET用于IPv4)in_port_t sin_port;      // 端口号(使用网络字节序)struct in_addr sin_addr; // IP地址char sin_zero[8];        // 保留字段,必须设置为0
};

对于IPv6地址,通常使用sockaddr_in6结构,该结构的定义如下:

struct sockaddr_in6 {sa_family_t sin6_family;   // 地址族(AF_INET6用于IPv6)in_port_t sin6_port;       // 端口号(使用网络字节序)uint32_t sin6_flowinfo;    // 流量信息struct in6_addr sin6_addr; // IP地址uint32_t sin6_scope_id;    // 作用域ID
};

在实际编程中,我们通常将特定类型的地址结构转换为sockaddr结构,并在使用时强制转换回原始类型。例如,在调用bind函数时,我们可以将sockaddr_in结构转换为sockaddr结构:

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
memset(addr.sin_zero, 0, sizeof(addr.sin_zero));bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

通过这种方式,我们可以实现代码的通用性,处理不同类型的地址结构,同时保证网络通信的正确性和可靠性。

2.3 in_addr结构

in_addr结构用于表示一个IPv4的IP地址,其实质是一个32位的整数。in_addr结构的定义如下:

struct in_addr {uint32_t s_addr;  // IP地址(使用网络字节序)
};

在网络编程中,我们通常使用点分十进制的字符串表示IP地址,例如"192.168.0.1"。为了在程序中使用这些字符串表示的IP地址,我们需要进行字符串和in_addr结构之间的转换。标准库提供了以下函数来进行这些转换:

  • inet_pton:将点分十进制的字符串转换为in_addr结构。
  • inet_ntop:将in_addr结构转换为点分十进制的字符串。

以下是这些函数的使用示例:

struct in_addr addr;
inet_pton(AF_INET, "192.168.0.1", &addr);char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, str, INET_ADDRSTRLEN);

通过这些函数,我们可以方便地在程序中处理IP地址,进行IP地址的转换和解析,确保网络通信的正确性。

三、简单的UDP网络程序

3.1 封装 UdpSocket

以下是一个简单的UdpSocket类的实现,它封装了UDP套接字的常见操作:

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cassert>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;class UdpSocket {
public:UdpSocket() : fd_(-1) {}bool Socket() {fd_ = socket(AF_INET, SOCK_DGRAM, 0);if (fd_ < 0) {perror("socket");return false;}return true;}bool Close() {close(fd_);return true;}bool Bind(const std::string& ip, uint16_t port) {sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));if (ret < 0) {perror("bind");return false;}return true;}bool RecvFrom(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL) {char tmp[1024 * 10] = {0};sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t read_size = recvfrom(fd_, tmp, sizeof(tmp) - 1, 0, (sockaddr*)&peer, &len);if (read_size < 0) {perror("recvfrom");return false;}buf->assign(tmp, read_size);if (ip != NULL) {*ip = inet_ntoa(peer.sin_addr);}if (port != NULL) {*port = ntohs(peer.sin_port);}return true;}bool SendTo(const std::string& buf, const std::string& ip, uint16_t port) {sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0, (sockaddr*)&addr, sizeof(addr));if (write_size < 0) {perror("sendto");return false;}return true;}private:int fd_;
};
3.2 UDP通用服务器

以下是一个通用的UDP服务器类UdpServer的实现:

#pragma once
#include "udp_socket.hpp"
#include <functional>typedef std::function<void (const std::string&, std::string* resp)> Handler;class UdpServer {
public:UdpServer() {assert(sock_.Socket());}~UdpServer() {sock_.Close();}bool Start(const std::string& ip, uint16_t port, Handler handler) {bool ret = sock_.Bind(ip, port);if (!ret) {return false;}for (;;) {std::string req;std::string remote_ip;uint16_t remote_port = 0;bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);if (!ret) {continue;}std::string resp;handler(req, &resp);sock_.SendTo(resp, remote_ip, remote_port);printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port, req.c_str(), resp.c_str());}sock_.Close();return true;}private:UdpSocket sock_;
};

基于以上封装,我们可以实现一个简单的英译汉服务器:

#include "udp_server.hpp"
#include <unordered_map>
#include <iostream>std::unordered_map<std::string, std::string> g_dict;void Translate(const std::string& req, std::string* resp) {auto it = g_dict.find(req);if (it == g_dict.end()) {*resp = "未查到!";return;}*resp = it->second;
}int main(int argc, char* argv[]) {if (argc != 3) {printf("Usage ./dict_server [ip] [port]\n");return 1;}g_dict.insert(std::make_pair("hello", "你好"));g_dict.insert(std::make_pair("world", "世界"));g_dict.insert(std::make_pair("c++", "最好的编程语言"));g_dict.insert(std::make_pair("bit", "特别NB"));UdpServer server;server.Start(argv[1], atoi(argv[2]), Translate);return 0;
}
3.3 UDP通用客户端

以下是一个通用的UDP客户端类UdpClient的实现:

#pragma once
#include "udp_socket.hpp"class UdpClient {
public:UdpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {assert(sock_.Socket());}~UdpClient() {sock_.Close();}bool RecvFrom(std::string* buf) {return sock_.RecvFrom(buf);}bool SendTo(const std::string& buf) {return sock_.SendTo(buf, ip_, port_);}private:UdpSocket sock_;std::string ip_;uint16_t port_;
};

基于以上封装,我们可以实现一个简单的英译汉客户端:

#include "udp_client.hpp"
#include <iostream>int main(int argc, char* argv[]) {if (argc != 3) {printf("Usage ./dict_client [ip] [port]\n");return 1;}UdpClient client(argv[1], atoi(argv[2]));for (;;) {std::string word;std::cout << "请输入您要查的单词: ";std::cin >> word;if (!std::cin) {std::cout << "Good Bye" << std::endl;break;}client.SendTo(word);std::string result;client.RecvFrom(&result);std::cout << word << " 意思是 " << result << std::endl;}return 0;
}

四、简单的TCP网络程序

4.1 TCP socket API 详解

TCP(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。以下是TCP socket API的详细介绍:

  • int socket(int domain, int type, int protocol);:创建一个新的TCP套接字。domain参数指定协议族,IPv4使用AF_INETtype参数指定套接字类型,TCP使用SOCK_STREAM
  • int bind(int socket, const struct sockaddr *address, socklen_t address_len);:将套接字绑定到本地地址和端口号。
  • int listen(int socket, int backlog);:将套接字设置为监听模式,backlog参数指定等待连接队列的最大长度。
  • int accept(int socket, struct sockaddr *address, socklen_t *address_len);:接受客户端连接请求,返回一个新的套接字用于与客户端通信。
  • int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);:与服务器建立连接。

这些API函数构成了TCP网络编程的基础,通过它们我们可以实现各种网络通信功能。例如,使用socket函数创建一个套接字,使用bind函数将其绑定到本地地址和端口号,使用listen函数将其设置为监听模式,使用accept函数接受客户端连接请求,使用connect函数与服务器建立连接。

在实际开发中,我们经常需要结合使用这些API函数来实现复杂的网络通信功能。例如,在实现一个TCP服务器时,我们可以使用socket函数创建一个服务器套接字,使用bind函数将其绑定到本地地址和端口号,使用listen函数将其设置为监听模式,然后使用accept函数接受客户端连接请求。在每个连接请求到来时,我们可以创建一个新的套接字用于与客户端通信,通过这个新的套接字接收和发送数据。

4.2 封装 TCP socket

以下是一个简单的TCP套接字封装类TcpSocket的实现:

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>#include <arpa/inet.h>
#include <fcntl.h>typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;#define CHECK_RET(exp) if (!(exp)) { return false; }class TcpSocket {
public:TcpSocket() : fd_(-1) {}TcpSocket(int fd) : fd_(fd) {}bool Socket() {fd_ = socket(AF_INET, SOCK_STREAM, 0);if (fd_ < 0) {perror("socket");return false;}printf("open fd = %d\n", fd_);return true;}bool Close() const {close(fd_);printf("close fd = %d\n", fd_);return true;}bool Bind(const std::string& ip, uint16_t port) const {sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));if (ret < 0) {perror("bind");return false;}return true;}bool Listen(int num) const {int ret = listen(fd_, num);if (ret < 0) {perror("listen");return false;}return true;}bool Accept(TcpSocket* peer, std::string* ip = NULL, uint16_t* port = NULL) const {sockaddr_in peer_addr;socklen_t len = sizeof(peer_addr);int new_sock = accept(fd_, (sockaddr*)&peer_addr, &len);if (new_sock < 0) {perror("accept");return false;}printf("accept fd = %d\n", new_sock);peer->fd_ = new_sock;if (ip != NULL) {*ip = inet_ntoa(peer_addr.sin_addr);}if (port != NULL) {*port = ntohs(peer_addr.sin_port);}return true;}bool Recv(std::string* buf) const {buf->clear();char tmp[1024 * 10] = {0};ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);if (read_size < 0) {perror("recv");return false;}if (read_size == 0) {return false;}buf->assign(tmp, read_size);return true;}bool Send(const std::string& buf) const {ssize_t write_size = send(fd_, buf.data(), buf.size(), 0);if (write_size < 0) {perror("send");return false;}return true;}bool Connect(const std::string& ip, uint16_t port) const {sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);int ret = connect(fd_, (sockaddr*)&addr, sizeof(addr));if (ret < 0) {perror("connect");return false;}return true;}int GetFd() const {return fd_;}private:int fd_;
};
4.3 TCP通用服务器

以下是一个通用的TCP服务器类TcpServer的实现:

#pragma once
#include <functional>
#include "tcp_socket.hpp"typedef std::function<void (const std::string& req, std::string* resp)> Handler;class TcpServer {
public:TcpServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {}bool Start(Handler handler) {CHECK_RET(listen_sock_.Socket());CHECK_RET(listen_sock_.Bind(ip_, port_));CHECK_RET(listen_sock_.Listen(5));for (;;) {TcpSocket new_sock;std::string ip;uint16_t port = 0;if (!listen_sock_.Accept(&new_sock, &ip, &port)) {continue;}printf("[client %s:%d] connect!\n", ip.c_str(), port);for (;;) {std::string req;bool ret = new_sock.Recv(&req);if (!ret) {printf("[client %s:%d] disconnect!\n", ip.c_str(), port);new_sock.Close();break;}std::string resp;handler(req, &resp);new_sock.Send(resp);printf("[%s:%d] req: %s, resp: %s\n", ip.c_str(), port, req.c_str(), resp.c_str());}}return true;}private:TcpSocket listen_sock_;std::string ip_;uint64_t port_;
};
4.4 英译汉服务器

基于以上封装,我们可以实现一个简单的英译汉服务器:

#include <unordered_map>
#include "tcp_server.hpp"std::unordered_map<std::string, std::string> g_dict;void Translate(const std::string& req, std::string* resp) {auto it = g_dict.find(req);if (it == g_dict.end()) {*resp = "未找到";return;}*resp = it->second;
}int main(int argc, char* argv[]) {if (argc != 3) {printf("Usage ./dict_server [ip] [port]\n");return 1;}g_dict.insert(std::make_pair("hello", "你好"));g_dict.insert(std::make_pair("world", "世界"));g_dict.insert(std::make_pair("bit", "贼NB"));TcpServer server(argv[1], atoi(argv[2]));server.Start(Translate);return 0;
}

五、简单的TCP网络程序(多进程版本)

通过每个请求创建子进程的方式来支持多连接,我们可以实现一个多进程版本的TCP服务器。以下是TcpProcessServer的实现:

#pragma once
#include <functional>
#include <signal.h>
#include "tcp_socket.hpp"typedef std::function<void (const std::string& req, std::string* resp)> Handler;class TcpProcessServer {
public:TcpProcessServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {signal(SIGCHLD, SIG_IGN);  // 忽略子进程的退出信号,避免产生僵尸进程}void ProcessConnect(const TcpSocket& new_sock, const std::string& ip, uint16_t port, Handler handler) {int ret = fork();  // 创建子进程if (ret > 0) {// 父进程new_sock.Close();return;} else if (ret == 0) {// 子进程for (;;) {std::string req;bool ret = new_sock.Recv(&req);if (!ret) {printf("[client %s:%d] disconnected!\n", ip.c_str(), port);exit(0);}std::string resp;handler(req, &resp);new_sock.Send(resp);printf("[client %s:%d] req: %s, resp: %s\n", ip.c_str(), port, req.c_str(), resp.c_str());}} else {perror("fork");}}bool Start(Handler handler) {CHECK_RET(listen_sock_.Socket());CHECK_RET(listen_sock_.Bind(ip_, port_));CHECK_RET(listen_sock_.Listen(5));for (;;) {TcpSocket new_sock;std::string ip;uint16_t port = 0;if (!listen_sock_.Accept(&new_sock, &ip, &port)) {continue;}printf("[client %s:%d] connect!\n", ip.c_str(), port);ProcessConnect(new_sock, ip, port, handler);}return true;}private:TcpSocket listen_sock_;std::string ip_;uint64_t port_;
};
5.1 多进程版本的实现分析

在上述实现中,我们通过fork()系统调用为每个新的客户端连接创建一个子进程。父进程负责监听和接受新的连接请求,而子进程则负责处理与客户端的具体通信。这样,通过多进程的方式,我们可以支持多个客户端同时连接和通信。

优点
  • 并发处理能力强:每个连接由独立的进程处理,可以充分利用多核CPU的性能。
  • 进程间隔离性强:进程之间相互独立,一个进程的崩溃不会影响到其他进程,增加了系统的稳定性。
缺点
  • 资源开销大:每创建一个子进程,操作系统都需要分配独立的内存和资源,资源开销较大。
  • 上下文切换开销大:进程之间的上下文切换比线程之间的上下文切换开销大,影响性能。

在实际开发中,使用多进程还是多线程需要根据具体场景进行选择。如果系统资源充足且需要高隔离性,可以选择多进程模式;如果需要高并发且资源受限,可以选择多线程模式。

六、简单的TCP网络程序(多线程版本)

通过每个请求创建一个线程的方式来支持多连接,我们可以实现一个多线程版本的TCP服务器。以下是TcpThreadServer的实现:

#pragma once
#include <functional>
#include <pthread.h>
#include "tcp_socket.hpp"typedef std::function<void (const std::string&, std::string*)> Handler;struct ThreadArg {TcpSocket new_sock;std::string ip;uint16_t port;Handler handler;
};class TcpThreadServer {
public:TcpThreadServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {}bool Start(Handler handler) {CHECK_RET(listen_sock_.Socket());CHECK_RET(listen_sock_.Bind(ip_, port_));CHECK_RET(listen_sock_.Listen(5));for (;;) {ThreadArg* arg = new ThreadArg();arg->handler = handler;bool ret = listen_sock_.Accept(&arg->new_sock, &arg->ip, &arg->port);if (!ret) {continue;}printf("[client %s:%d] connect\n", arg->ip.c_str(), arg->port);pthread_t tid;pthread_create(&tid, NULL, ThreadEntry, arg);pthread_detach(tid);  // 分离线程,自动回收资源}return true;}static void* ThreadEntry(void* arg) {ThreadArg* p = reinterpret_cast<ThreadArg*>(arg);ProcessConnect(p);p->new_sock.Close();delete p;return NULL;}static void ProcessConnect(ThreadArg* arg) {for (;;) {std::string req;bool ret = arg->new_sock.Recv(&req);if (!ret) {printf("[client %s:%d] disconnected!\n", arg->ip.c_str(), arg->port);break;}std::string resp;arg->handler(req, &resp);arg->new_sock.Send(resp);printf("[client %s:%d] req: %s, resp: %s\n", arg->ip.c_str(), arg->port, req.c_str(), resp.c_str());}}private:TcpSocket listen_sock_;std::string ip_;uint16_t port_;
};
6.1 多线程版本的实现分析

在上述实现中,我们通过pthread_create()系统调用为每个新的客户端连接创建一个线程。主线程负责监听和接受新的连接请求,而新创建的线程则负责处理与客户端的具体通信。这样,通过多线程的方式,我们可以支持多个客户端同时连接和通信。

优点
  • 并发处理能力强:每个连接由独立的线程处理,可以充分利用多核CPU的性能。
  • 资源开销小:线程共享进程的内存和资源,相比多进程模式,资源开销较小。
  • 上下文切换开销小:线程之间的上下文切换比进程之间的上下文切换开销小,性能更高。
缺点
  • 线程安全问题:多个线程共享进程的内存和资源,需要注意线程安全问题,可能导致数据竞争和死锁等问题。
  • 线程数量有限:系统对线程的数量有一定限制,过多的线程可能导致资源耗尽,影响系统稳定性。

在实际开发中,使用多线程模式可以在一定程度上提高并发处理能力,但需要注意线程安全问题,合理控制线程数量,避免资源耗尽。

七、TCP协议通讯流程

7.1 建立连接的过程

TCP协议的连接建立过程称为三次握手:

  1. SYN:客户端向服务器发送一个SYN报文,表示请求建立连接。
  2. SYN-ACK:服务器收到SYN报文后,回应一个SYN-ACK报文,表示同意建立连接。
  3. ACK:客户端收到SYN-ACK报文后,回应一个ACK报文,表示连接建立完成。

在三次握手过程中,双方需要交换序列号和确认号,以确保连接的可靠性和数据的有序性。具体过程如下:

  • 客户端发送SYN报文,设置序列号为x
  • 服务器收到SYN报文,发送SYN-ACK报文,设置序列号为y,确认号为x+1
  • 客户端收到SYN-ACK报文,发送ACK报文,确认号为y+1

通过三次握手,客户端和服务器建立了可靠的连接,后续可以进行数据传输。

7.2 数据传输的过程

TCP协议提供全双工通信服务,通信双方可以同时发送和接收数据。服务器从accept()返回后,可以调用read()读取客户端发送的数据,同时客户端可以调用write()发送请求。

在数据传输过程中,TCP协议通过序列号和确认号确保数据的有序性和完整性。具体过程如下:

  • 发送方将数据分成若干数据段,每个数据段都有一个序列号。
  • 接收方收到数据段后,发送一个确认报文(ACK),确认号为已接收到的数据段的序列号加1。
  • 发送方在接收到确认报文后,继续发送后续的数据段。
  • 如果发送方在一定时间内没有收到确认报文,会进行数据重传,直到接收到确认报文为止。

通过这种方式,TCP协议保证了数据的可靠传输,避免数据丢失和重复。

7.3 断开连接的过程

TCP协议的连接断开过程称为

四次挥手:

  1. FIN:客户端向服务器发送一个FIN报文,表示请求断开连接。
  2. ACK:服务器收到FIN报文后,回应一个ACK报文,表示同意断开连接。
  3. FIN:服务器向客户端发送一个FIN报文,表示准备断开连接。
  4. ACK:客户端收到FIN报文后,回应一个ACK报文,表示断开连接完成。

在四次挥手过程中,双方需要交换序列号和确认号,以确保连接的可靠断开。具体过程如下:

  • 客户端发送FIN报文,设置序列号为x
  • 服务器收到FIN报文,发送ACK报文,确认号为x+1
  • 服务器发送FIN报文,设置序列号为y
  • 客户端收到FIN报文,发送ACK报文,确认号为y+1

通过四次挥手,客户端和服务器断开了连接,释放了相关资源。

八、TCP 和 UDP 对比

8.1 可靠传输 vs 不可靠传输

TCP提供可靠的数据传输,通过确认和重传机制保证数据的完整性和顺序性;UDP则不保证数据的传输可靠性,数据可能丢失、重复或乱序。

TCP通过序列号、确认号和重传机制,确保数据在传输过程中不丢失、不重复和按顺序到达接收方。这使得TCP适用于对传输可靠性要求高的场景,如文件传输、电子邮件等。

UDP则不提供这些保证,数据报在传输过程中可能会丢失或乱序。因此,UDP适用于对实时性要求高但对传输可靠性要求低的场景,如视频直播、在线游戏等。

8.2 有连接 vs 无连接

TCP是面向连接的协议,在传输数据前需要建立连接;UDP是无连接的协议,每个数据报都是独立的,不需要建立连接。

TCP在传输数据前,通过三次握手建立连接,确保双方可以进行可靠的数据传输。建立连接后,双方可以通过连接进行全双工通信,传输数据和确认报文。

UDP则不需要建立连接,每个数据报都是独立的,可以直接发送和接收。由于不需要建立连接,UDP的传输开销较小,适用于对实时性要求高的场景。

8.3 字节流 vs 数据报

TCP是面向字节流的协议,数据被视为连续的字节流进行传输;UDP是面向数据报的协议,每个数据报都是一个独立的消息。

TCP在传输数据时,将数据分成若干数据段,每个数据段都有一个序列号。接收方在接收到数据段后,将其按序号排列成连续的字节流,确保数据的有序性和完整性。

UDP则将数据分成若干数据报,每个数据报都是一个独立的消息。接收方在接收到数据报后,可以直接处理每个数据报,而不需要按顺序排列。这使得UDP适用于对实时性要求高但对有序性和完整性要求低的场景。

九、总结

通过本文,我们详细介绍了网络编程套接字的基础知识,包括IP地址、端口号、TCP和UDP协议、网络字节序等内容。随后,我们通过封装的方式实现了简单的UDP和TCP网络程序,并探讨了多进程和多线程版本的实现方式。最后,我们详细讲解了TCP协议的连接建立、数据传输和连接断开过程,以及TCP和UDP协议的对比。

在实际开发中,选择使用TCP还是UDP取决于具体的应用场景和需求。如果需要可靠的数据传输和连接控制,可以选择TCP协议;如果需要低延迟和高实时性,可以选择UDP协议。通过理解和掌握这些基础知识和编程技巧,我们可以更好地进行网络编程,开发出高效、可靠的网络应用。

嗯,就是这样啦,文章到这里就结束啦,真心感谢你花时间来读。
觉得有点收获的话,不妨给我点个吧!
如果发现文章有啥漏洞或错误的地方,欢迎私信我或者在评论里提醒一声~

相关文章:

从入门到精通:网络编程套接字(万字详解,小白友好,建议收藏)

一、预备知识 1.1 理解源IP地址和目的IP地址 在网络编程中&#xff0c;IP地址&#xff08;Internet Protocol Address&#xff09;是每个连接到互联网的设备的唯一标识符。IP地址可以分为IPv4和IPv6两种类型。IPv4地址是由32位二进制数表示&#xff0c;通常分为四个八位组&am…...

dledger原理源码分析系列(一)架构,核心组件和rpc组件

简介 dledger是openmessaging的一个组件&#xff0c; raft算法实现&#xff0c;用于分布式日志&#xff0c;本系列分析dledger如何实现raft概念&#xff0c;以及dledger在rocketmq的应用 本系列使用dledger v0.40 本文分析dledger的架构&#xff0c;核心组件&#xff1b;rpc组…...

第七节:如何浅显易懂地理解Spring Boot中的依赖注入(自学Spring boot 3.x的第二天)

大家好&#xff0c;我是网创有方&#xff0c;今天我开始学习spring boot的第一天&#xff0c;一口气写了这么多。 这节通过一个非常浅显易懂的列子来讲解依赖注入。 在Spring Boot 3.x中&#xff0c;依赖注入&#xff08;Dependency Injection, DI&#xff09;是一个核心概念…...

Postman自动化测试实战:使用脚本提升测试效率

在软件开发过程中&#xff0c;接口测试是确保后端服务稳定性和可靠性的关键步骤。Postman作为一个流行的API开发工具&#xff0c;提供了强大的脚本功能来实现自动化测试。通过在Postman中使用脚本&#xff0c;测试人员可以编写测试逻辑&#xff0c;实现测试用例的自动化执行&am…...

CSMA/CA并不是“公平”的

CSMA/CA会造成过于公平,对于最需要流量的节点,是最不友好的,而对于最不需要流量的节点,则是最友好的。 CSMA/CA是优先公平来工作的。 CSMA/CA首先各节点使用DIFS界定air idle,在此期间大家都等待 其次,为了同时发送引起碰撞,在DIFS之后随机从CWmin和CWmax之间选择一个时…...

【漏洞复现】I doc view——任意文件读取

声明&#xff1a;本文档或演示材料仅供教育和教学目的使用&#xff0c;任何个人或组织使用本文档中的信息进行非法活动&#xff0c;均与本文档的作者或发布者无关。 文章目录 漏洞描述漏洞复现测试工具 漏洞描述 I doc view 在线文档预览是一个用于查看、编辑、管理文档的工具…...

图数据库 vs 向量数据库

最近大模型出来之后&#xff0c;向量数据库重新翻红&#xff0c;业界和市场上有不少声音认为向量数据库会极大的影响图数据库&#xff0c;图数据库市场会萎缩甚至消失&#xff0c;今天就从技术原理角度来讨论下图数据库和向量数据库到底差别在哪里&#xff0c;适合什么场景&…...

企业品牌出海第一站 维基百科词条创建

维基百科是一部内容开放、自由的网络百科全书,旨在创造一个涵盖所有领域知识,服务所有互联网用户的知识性百科全书。其在国外应用非常广泛且认可度很高&#xff0c;国内品牌出海或国际品牌都很有必要创建企业自己的维基百科页面&#xff0c;以及企业高管的个人维基百科页面。 如…...

Windows下activemq集群配置(broker-network)

1.activemq版本信息 activemq&#xff1a;apache-activemq-5.18.4 2.activemq架构 3.activemq集群配置 activemq集群配置基于Networks of Brokers 这种HA方案的优点&#xff1a;是占用的节点数更少(只需要2个节点),而且2个broker都可以响应消息的接收与发送。不足&#xff…...

心理辅导平台系统

摘 要 中文本论文基于Java Web技术设计与实现了一个心理辅导平台。通过对国内外心理辅导平台发展现状的调研&#xff0c;本文分析了心理辅导平台的背景与意义&#xff0c;并提出了论文研究内容与创新点。在相关技术介绍部分&#xff0c;对Java Web、SpringBoot、B/S架构、MVC模…...

代理IP对SEO影响分析:提升网站排名的关键策略

你是否曾经为网站排名难以提升而苦恼&#xff1f;代理服务器或许就是你忽略的关键因素。在竞争激烈的互联网环境中&#xff0c;了解代理服务器对SEO的影响&#xff0c;有助于你采取更有效的策略&#xff0c;提高网站的搜索引擎排名。本文将为你详细分析代理服务器在SEO优化中的…...

【leetcode--三数之和】

这道题记得之前做过&#xff0c;但是想不起来了。。总结一下&#xff1a; 函数的主要步骤和关键点&#xff1a; 排序&#xff1a;对输入的整数数组nums进行排序。这是非常重要的&#xff0c;因为它允许我们使用双指针技巧来高效地找到满足条件的三元组。初始化&#xff1a;定…...

解决Java中的ClassCastException问题

解决Java中的ClassCastException问题 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 在Java编程中&#xff0c;ClassCastException是一个常见的运行时异常&am…...

【TensorFlow深度学习】混合生成模型:结合AR与AE的创新尝试

混合生成模型&#xff1a;结合AR与AE的创新尝试 引言自回归模型与自动编码器的简述混合模型的创新尝试组合AR与AE&#xff1a;MADE混合模型在图学习中的应用 结论与展望 在自我监督学习的广阔天地里&#xff0c;混合生成模型以其独特的魅力&#xff0c;跨越了自回归&#xff08…...

Spring:Spring中分布式事务解决方案

一、前言 在Spring中&#xff0c;分布式事务是指涉及多个数据库或系统的事务处理&#xff0c;其中事务的参与者、支持事务的服务器、资源管理器以及事务管理器位于分布式系统的不同节点上。这样的架构使得两个或多个网络计算机上的数据能够被访问并更新&#xff0c;同时将这些操…...

音视频开发32 FFmpeg 编码- 视频编码 h264 参数相关

1. ffmpeg -h 这个命令总不会忘记&#xff0c;用这个先将ffmpeg所有的help信息都list出来 C:\Users\Administrator>ffmpeg -h ffmpeg version 6.0-full_build-www.gyan.dev Copyright (c) 2000-2023 the FFmpeg developersbuilt with gcc 12.2.0 (Rev10, Built by MSYS2 pro…...

标准版小程序订单中心path审核不通过处理教程

首先看自己小程序是不是已经审核通过并上线状态才在站内信里面提醒的&#xff1f; 如果没有提交过审核&#xff0c;请在提交的时候填写。path地址为&#xff1a;pages/goods/order_list/index 如果是已经上线的小程序&#xff0c;当时没要求填这个&#xff0c;但新的政策要求填…...

移植对话框MFC

VC版 MFC程序对话框资源移植 以下均拷贝自上面&#xff0c;仅用来记录 &#xff08;部分有删除&#xff09; 法1&#xff1a; Eg&#xff1a;将B工程调试好的对话框移植到A工程中 1.资源移植 1.1 在2017打开B工程,在工作区Resource标签页中选中Dialog文件夹下的资源文件,按…...

【开源的字典项目】【macOS】:在macOS上能打开mdd and mdx 的github开源项目

【开源的字典项目】【macOS】 在macOS上能打开mdd and mdx 的github开源项目 Here are some GitHub repositories that provide code for opening and reading mdd and mdx files in macOS: 1. MdxEdit: Repository: https://github.com/mdx-editorDescription: A free and …...

已解决javax.security.auth.login.LoginException:登录失败的正确解决方法,亲测有效!!!

已解决javax.security.auth.login.LoginException&#xff1a;登录失败的正确解决方法&#xff0c;亲测有效&#xff01;&#xff01;&#xff01; 目录 问题分析 出现问题的场景 报错原因 解决思路 解决方法 1. 检查用户名和密码 用户名和密码验证 2. 验证配置文件 …...

web vue 项目 Docker化部署

Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段&#xff1a; 构建阶段&#xff08;Build Stage&#xff09;&#xff1a…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互

物理引擎&#xff08;Physics Engine&#xff09; 物理引擎 是一种通过计算机模拟物理规律&#xff08;如力学、碰撞、重力、流体动力学等&#xff09;的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互&#xff0c;广泛应用于 游戏开发、动画制作、虚…...

【位运算】消失的两个数字(hard)

消失的两个数字&#xff08;hard&#xff09; 题⽬描述&#xff1a;解法&#xff08;位运算&#xff09;&#xff1a;Java 算法代码&#xff1a;更简便代码 题⽬链接&#xff1a;⾯试题 17.19. 消失的两个数字 题⽬描述&#xff1a; 给定⼀个数组&#xff0c;包含从 1 到 N 所有…...

学校招生小程序源码介绍

基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码&#xff0c;专为学校招生场景量身打造&#xff0c;功能实用且操作便捷。 从技术架构来看&#xff0c;ThinkPHP提供稳定可靠的后台服务&#xff0c;FastAdmin加速开发流程&#xff0c;UniApp则保障小程序在多端有良好的兼…...

【论文笔记】若干矿井粉尘检测算法概述

总的来说&#xff0c;传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度&#xff0c;通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

#Uniapp篇:chrome调试unapp适配

chrome调试设备----使用Android模拟机开发调试移动端页面 Chrome://inspect/#devices MuMu模拟器Edge浏览器&#xff1a;Android原生APP嵌入的H5页面元素定位 chrome://inspect/#devices uniapp单位适配 根路径下 postcss.config.js 需要装这些插件 “postcss”: “^8.5.…...

安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖

在Vuzix M400 AR智能眼镜的助力下&#xff0c;卢森堡罗伯特舒曼医院&#xff08;the Robert Schuman Hospitals, HRS&#xff09;凭借在无菌制剂生产流程中引入增强现实技术&#xff08;AR&#xff09;创新项目&#xff0c;荣获了2024年6月7日由卢森堡医院药剂师协会&#xff0…...

Kafka入门-生产者

生产者 生产者发送流程&#xff1a; 延迟时间为0ms时&#xff0c;也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于&#xff1a;异步发送不需要等待结果&#xff0c;同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...

xmind转换为markdown

文章目录 解锁思维导图新姿势&#xff1a;将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件&#xff08;ZIP处理&#xff09;2.解析JSON数据结构3&#xff1a;递归转换树形结构4&#xff1a;Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...