从入门到精通:网络编程套接字(万字详解,小白友好,建议收藏)
一、预备知识
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协议的连接建立过程称为三次握手:
- SYN:客户端向服务器发送一个SYN报文,表示请求建立连接。
- SYN-ACK:服务器收到SYN报文后,回应一个SYN-ACK报文,表示同意建立连接。
- ACK:客户端收到SYN-ACK报文后,回应一个ACK报文,表示连接建立完成。
TCP协议的连接断开过程称为四次挥手:
- FIN:客户端向服务器发送一个FIN报文,表示请求断开连接。
- ACK:服务器收到FIN报文后,回应一个ACK报文,表示同意断开连接。
- FIN:服务器向客户端发送一个FIN报文,表示准备断开连接。
- 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_INET
,type
参数指定套接字类型,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协议的连接建立过程称为三次握手:
- SYN:客户端向服务器发送一个SYN报文,表示请求建立连接。
- SYN-ACK:服务器收到SYN报文后,回应一个SYN-ACK报文,表示同意建立连接。
- 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协议的连接断开过程称为
四次挥手:
- FIN:客户端向服务器发送一个FIN报文,表示请求断开连接。
- ACK:服务器收到FIN报文后,回应一个ACK报文,表示同意断开连接。
- FIN:服务器向客户端发送一个FIN报文,表示准备断开连接。
- 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地址 在网络编程中,IP地址(Internet Protocol Address)是每个连接到互联网的设备的唯一标识符。IP地址可以分为IPv4和IPv6两种类型。IPv4地址是由32位二进制数表示,通常分为四个八位组&am…...

dledger原理源码分析系列(一)架构,核心组件和rpc组件
简介 dledger是openmessaging的一个组件, raft算法实现,用于分布式日志,本系列分析dledger如何实现raft概念,以及dledger在rocketmq的应用 本系列使用dledger v0.40 本文分析dledger的架构,核心组件;rpc组…...

第七节:如何浅显易懂地理解Spring Boot中的依赖注入(自学Spring boot 3.x的第二天)
大家好,我是网创有方,今天我开始学习spring boot的第一天,一口气写了这么多。 这节通过一个非常浅显易懂的列子来讲解依赖注入。 在Spring Boot 3.x中,依赖注入(Dependency Injection, DI)是一个核心概念…...

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

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

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

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

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

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

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

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

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

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

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

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

音视频开发32 FFmpeg 编码- 视频编码 h264 参数相关
1. ffmpeg -h 这个命令总不会忘记,用这个先将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审核不通过处理教程
首先看自己小程序是不是已经审核通过并上线状态才在站内信里面提醒的? 如果没有提交过审核,请在提交的时候填写。path地址为:pages/goods/order_list/index 如果是已经上线的小程序,当时没要求填这个,但新的政策要求填…...

移植对话框MFC
VC版 MFC程序对话框资源移植 以下均拷贝自上面,仅用来记录 (部分有删除) 法1: Eg:将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:登录失败的正确解决方法,亲测有效!!! 目录 问题分析 出现问题的场景 报错原因 解决思路 解决方法 1. 检查用户名和密码 用户名和密码验证 2. 验证配置文件 …...

2741. 特别的排列 Medium
给你一个下标从 0 开始的整数数组 nums ,它包含 n 个 互不相同 的正整数。如果 nums 的一个排列满足以下条件,我们称它是一个特别的排列: 对于 0 < i < n - 1 的下标 i ,要么 nums[i] % nums[i1] 0 ,要么 nums[…...

读AI新生:破解人机共存密码笔记15辅助博弈
1. 辅助博弈 1.1. assistance game 1.2. 逆强化学习如今已经是构建有效的人工智能系统的重要工具,但它做了一些简化的假设 1.2.1. 机器人一旦通过观察人类学会了奖励函数,它就会采用奖励函数,这样它就可以执行相同的任务 1.2.1.1. 解决这…...

C++ 因项目需求,需要将0~2的32次方这个区间的数字保存到内存当中(内存大小为4G),并且可以实现对任意一个数字的增删。(先叙述设计思路,再写岀代码)
问题: C 因项目需求,需要将0~2的32次方这个区间的数字保存到内存当中(内存大小为4G),并且可以实现对任意一个数字的增删。(先叙述设计思路,再写岀代码) 解答 设计思路代码实现说明 为了在有限的内存(4GB)中存储和操作 …...

Linux 下的性能监控与分析技巧
在日常的服务器管理和问题诊断过程中,Linux 命令行工具提供了强大的支持。本文通过几个常用的示例,介绍如何快速定位问题、监控服务器性能。 无论你是编程新手还是有一定经验的开发者,理解和掌握这些命令,都将在你的工作中大放异…...

不可复制网站上的文字——2种方法
禁用javascript或Console控制台代码 (1)F12键——设置——勾选禁用javascript (2)Console控制台敲如下代码: var allowPaste function(e){ e.stopImmediatePropagation(); return true; }; document.addEventListe…...

Ubuntu 22.04上编译安装c++ spdlog library
Very fast, header-only/compiled, C logging library. 请以root身份或sudo执行。 1. 安装必需的依赖项: sudo apt-get update sudo apt-get install git g cmake 2. 克隆 spdlog 仓库: cd /opt git clone https://github.com/gabime/spdlog.git …...

ESP32代码开发入门
ESP-IDF ESP-ADF开发 开发概要 编译环境及SDK搭建 整个开发流程是:下载ESP-IDF, ESP-ADF(按需下载),并安装, 编写hello world工程,编译并烧录到主板验证 可参照ESP32 esp-idf esp-adf环境安装及.a库创建与编译api大部分可以用glibc的接口 做了封装,时间time(NULL), 创建线程p…...

“势”是“态”的偶然性减少
“态势感知”中的“势”指的是一种趋势或倾向性,而“态”则表示状态或局势。这个术语常用于描述在一段时间内系统或事件显示出来的方向性变化或发展趋势。因此,可以将“态势”理解为系统或事件状态变化的趋势,这种变化通常反映出偶然性减少的…...

人脑计算机技术与Neuroplatform:未来计算的革命性进展
引言 想象一下,你在某个清晨醒来,准备开始一天的工作,而实际上你的大脑正作为一台生物计算机的核心,处理着大量复杂的信息。这并非科幻电影的情节,而是人脑计算机技术即将带来的现实。本文将深入探讨FinalSpark公司的…...

新版周易测算系统源码 去授权完美运行
已经去掉授权可以完美运行 更新了三个模板市面上都是几千几千的卖 更新了三套首页新ui 自己后台切换就行 源码大小:338M 源码下载:https://download.csdn.net/download/m0_66047725/89447857 更多资源下载:关注我....