深入理解 TCP 套接字:Socket 编程入门教程
个人主页:chian-ocean
文章专栏-NET
深入理解 TCP 套接字:Socket 编程入门教程
- 个人主页:chian-ocean
- 文章专栏-NET
- 前言
- TCP工作原理
- 建立连接(三次握手)
- 中断连接(四次挥手)
- TCP `socket`接口
- 创建套接字(`socket`)
- 参数说明:
- 绑定地址(`bind`)
- 监听连接 (`listen`)
- 接受连接(`accept`)
- **数据传输** (`read` 和`write` )
- `read`
- `write`
- **关闭连接** (`close`)
- TCP`socket`编程
- 1. **包含的头文件**
- 2. **枚举类型 `err`**
- 3. **`ThreadData` 类**
- 4. **`TcpServer` 类**
- 成员变量
- 构造函数和析构函数
- 初始化方法 `Init()`
- 服务器运行方法 `Run()`
- 处理客户端请求方法 `Service()`
- 5. **多进程、多线程和线程池处理**
- **单进程**:直接调用 `Service()` 方法处理请求。
- **多进程**:使用 `fork()` 创建子进程来处理每个客户端请求。
- **多线程**:使用 `pthread_create()` 创建线程来处理请求。
- **线程池**:通过线程池(`ThreadPool`)将客户端的处理任务添加到队列中,线程池会负责处理这些任务。
- `Linux`客户端
- 1. **命令行参数处理**
- 2. **创建套接字并连接服务器**
- 3. **消息发送与接收**
- 4. **退出机制**
- 测试
- `Windows`客户端
- 代码工作流程:
- 测试:
- Linux源码
- Windows源码
前言
TCP套接字(TCP Socket)是网络编程中用于实现基于TCP协议的通信的一种机制。TCP(传输控制协议)是一个面向连接、可靠的传输层协议,保证数据的可靠性、顺序性和完整性。TCP套接字就是应用程序与TCP协议栈之间的接口,允许应用程序在网络中发送和接收数据。
TCP工作原理
建立连接(三次握手)
在数据传输之前,TCP需要建立一条可靠的连接。这个过程称为“三次握手”(Three-Way Handshake):
- 第一步:客户端发送一个SYN(同步)包到服务器,表明希望建立连接。
- 第二步:服务器收到SYN包后,回复一个SYN-ACK(同步-确认)包,表示同意建立连接。
- 第三步:客户端收到SYN-ACK包后,回复一个ACK(确认)包,连接建立完成。
中断连接(四次挥手)
- 当数据传输完成后,双方需要关闭连接。关闭过程通过“四次挥手”(Four-Way Handshake)来完成:
- 一方(客户端或服务器)发送FIN(结束)包,表示没有数据发送了。
- 对方确认FIN包,发送ACK包。
- 对方也发送FIN包,表示关闭连接。
- 初始发送方确认对方的FIN包,并发送ACK包,连接关闭。
TCP socket
接口
创建套接字(socket
)
int socket(int domain, int type, int protocol);
参数说明:
- domain:指定协议族,通常使用
AF_INET
表示 IPv4,AF_INET6
表示 IPv6,AF_UNIX
表示 UNIX 域套接字。 - type:指定套接字类型,通常使用:
SOCK_STREAM
:流式套接字,适用于 TCP 协议(面向连接)。SOCK_DGRAM
:数据报套接字,适用于 UDP 协议(无连接)。
- protocol:指定协议类型,通常设为 0,表示自动选择合适的协议。
绑定地址(bind
)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
: 这是一个已创建的套接字文件描述符,它是通过socket()
系统调用返回的。addr
: 指向一个sockaddr
结构体的指针,包含套接字所需绑定的地址信息。该结构体通常包括 IP 地址和端口号。addrlen
:addr
结构体的长度,以字节为单位。
监听连接 (listen
)
int listen(int sockfd, int backlog);
参数:
-
sockfd
:套接字描述符,用于标识你希望监听连接请求的套接字。 -
backlog
:这个参数指定了待处理连接的队列的最大长度。也就是说,它定义了在处理接收到的连接请求之前,可以有多少个连接请求在等待队列中排队。
接受连接(accept
)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
-
sockfd
: 服务器端套接字的文件描述符(通过socket()
创建,并通过bind()
和listen()
配置)。accept()
会等待这个套接字的连接请求。 -
addr
: 一个指向sockaddr
结构的指针,用于存放连接到服务器的客户端的地址信息。可以使用sockaddr_in
(IPv4 地址)或sockaddr_in6
(IPv6 地址)结构来存储地址。 -
addrlen
: 用于指定addr
结构的大小。它在调用accept()
后会被更新为实际存储的客户端地址长度。
数据传输 (read
和write
)
read
ssize_t read(int fd, void *buf, size_t count);
参数:
-
fd
: 文件描述符(file descriptor)。它是一个整数,标识打开的文件、管道、套接字或其他 I/O 通道。通常,0
是标准输入(stdin),1
是标准输出(stdout),2
是标准错误输出(stderr)。调用open()
函数可以获得文件描述符。 -
buf
: 指向一个缓冲区的指针,read()
会将读取的数据存储在该缓冲区中。缓冲区应该足够大,以容纳指定数量的字节。 -
count
: 要读取的字节数,即期望从文件描述符fd
中读取的最大字节数。
write
ssize_t write(int fd, const void *buf, size_t count);
参数:
-
fd
: 文件描述符(file descriptor)。标识要写入的目标文件或设备。可以是由open()
打开的文件,也可以是标准输出(1
)、标准错误(2
)等文件描述符。 -
buf
: 一个指向数据缓冲区的指针,write()
会将缓冲区中的数据写入到指定的文件描述符中。buf
指向的数据是要写入的原始字节数据。 -
count
: 要写入的字节数,即希望将多少字节的数据从buf
写入到文件描述符fd
。
关闭连接 (close
)
int close(int fd);
- 参数:
fd
是一个整型值,表示要关闭的文件描述符。
TCPsocket
编程
这个 C++ 代码实现了一个简单的 TCP 服务器,它可以通过多种方式处理客户端的请求:单进程、子进程、线程、线程池。下面我将对各个部分做详细的分析和注释。
1. 包含的头文件
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
#include <strings.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
这些头文件包含了 C++ 中进行网络通信、进程管理、线程处理等相关操作所需要的函数和数据类型。
unistd.h
:包含了处理系统调用的函数(如read()
、write()
、close()
等)。sys/wait.h
:用于处理子进程,提供了waitpid()
函数。cstring
和strings.h
:用于处理字符串操作(如memset()
、bzero()
)。pthread.h
:用于多线程操作,提供了线程创建、管理、同步等函数。sys/socket.h
:网络编程相关函数和数据类型。netinet/in.h
和arpa/inet.h
:用于处理网络地址(IP 地址和端口)等。
2. 枚举类型 err
enum err
{SocketErr = 1,BindErr,ListenErr,
};
这个枚举类型定义了三种错误类型:SocketErr
、BindErr
和 ListenErr
,分别对应套接字创建、绑定和监听过程中的错误。
3. ThreadData
类
class ThreadData
{
public:ThreadData(int sockfd, const uint16_t &clientport, const std::string clientip, TcpServer *t): sockfd_(sockfd), port_(clientport), ip_(clientip), tser_(t){}public:int sockfd_;uint16_t port_;std::string ip_;TcpServer *tser_;
};
这个类用于保存线程执行所需的数据,包括客户端的套接字、端口、IP 地址和 TcpServer
对象的指针。它作为多线程处理中的参数传递给线程函数。
4. TcpServer
类
这是整个服务器的核心类,负责初始化和管理服务器的套接字、绑定、监听以及处理客户端的请求。
成员变量
int sockfd_;
uint16_t port_;
std::string ip_;
sockfd_
:服务器套接字的文件描述符。port_
:服务器监听的端口。ip_
:服务器监听的 IP 地址。
构造函数和析构函数
TcpServer(uint16_t port = defaultport, std::string ip = defaultip): port_(port), ip_(ip)
{
}~TcpServer()
{if (sockfd_ > 0)close(sockfd_);
}
构造函数初始化 port_
和 ip_
,如果未提供参数,则使用默认端口 8080
和默认 IP 0.0.0.0
。
初始化方法 Init()
int Init()
{// 创建套接字sockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (sockfd_ < 0){lg(FATAL, "Socket Error,errno:%d,error", errno, strerror(errno));return SocketErr;}lg(INFO, "Socket Success");int opt = 1;setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));// bindstruct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port_);inet_aton(ip_.c_str(), &server.sin_addr);int bindn = bind(sockfd_, (struct sockaddr *)&server, sizeof(server));if (bindn < 0){lg(FATAL, "Bind Error,errno:%d,error:%s", errno, strerror(errno));return BindErr;}lg(INFO, "Bind Success");// 监听int listenn = listen(sockfd_, backlog);if (listenn < 0){lg(FATAL, "Listen Error,errno:%d,error:%s", errno, strerror(errno));return ListenErr;}lg(INFO, "Listen Success");
}
- 创建套接字:使用
socket()
创建一个 TCP 套接字。 - 设置套接字选项:使用
setsockopt()
设置套接字选项,允许端口复用(SO_REUSEADDR | SO_REUSEPORT
)。 - 绑定套接字:使用
bind()
将套接字与指定的 IP 地址和端口绑定。 - 监听套接字:使用
listen()
开始监听传入的连接。
服务器运行方法 Run()
void Run()
{lg(INFO, "TCP running...");ThreadPool<Task>::GetInstance()->start();lg(INFO,"ThreadPool Start...");for (;;){struct sockaddr_in client;bzero(&client, sizeof(client));socklen_t clientlen = sizeof(client);int sockfd = accept(sockfd_, (struct sockaddr *)&client, &clientlen);if (sockfd < 0){continue;}uint16_t clientport = ntohs(client.sin_port);char clientip1[32];inet_ntop(AF_INET, &(client.sin_addr), clientip1, sizeof(client));std::string clientip = clientip1;lg(INFO, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);// 选择处理方式ThreadPolls(sockfd, clientport, clientip);}
}
- 等待客户端连接:使用
accept()
等待并接收客户端的连接。 - 获取客户端信息:获取客户端的 IP 和端口。
- 处理客户端请求:将客户端信息传递给线程池进行处理。
处理客户端请求方法 Service()
void Service(int sockfd, const uint16_t &clientport, const std::string clientip)
{char buffer[4096];while (true){ssize_t n = read(sockfd, &buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;std::cout << "Client say# " << buffer << std::endl;std::string echo = "TcpServer ehco# ";echo += buffer;write(sockfd, echo.c_str(), echo.size());}else if (n == 0){lg(INFO, "Server Quit..");break;}else if (n < 0){lg(WARNING, "read error");break;}}
}
这个方法会持续读取客户端发送的数据,并将接收到的数据回传给客户端,直到客户端断开连接。
5. 多进程、多线程和线程池处理
单进程:直接调用 Service()
方法处理请求。
// 单进程
void SingleProcess(int sockfd, const uint16_t &clientport, const std::string clientip)
{Service(sockfd, clientport, clientip);close(sockfd);
}
多进程:使用 fork()
创建子进程来处理每个客户端请求。
// 多进程
void MultiProcess(int sockfd, const uint16_t &clientport, const std::string clientip)
{pid_t id = fork();if (id == 0){lg(INFO, "Child Process Create...");close(sockfd_);if (fork() > 0)exit(0); // 进程孤儿Service(sockfd, clientport, clientip);close(sockfd);exit(0);}// fatherclose(sockfd);int n = waitpid(id, nullptr, 0);if (n > 0)lg(INFO, "Child Process Quit...");
}
多线程:使用 pthread_create()
创建线程来处理请求。
// 多线程
void MultiThreads(int sockfd, const uint16_t &clientport, const std::string& clientip)
{pthread_t tid;ThreadData *td = new ThreadData(sockfd, clientport, clientip, this);pthread_create(&tid, nullptr, Routine, td);
}
static void *Routine(void *argv)
{pthread_detach(pthread_self()); // 线程分离ThreadData *td = static_cast<ThreadData *>(argv);td->tser_->Service(td->sockfd_, td->port_, td->ip_);delete td;return nullptr;
}
线程池:通过线程池(ThreadPool
)将客户端的处理任务添加到队列中,线程池会负责处理这些任务。
- 这块需要接入线程池的
API
//线程池
void ThreadPolls(int sockfd, uint16_t &clientport,std::string clientip)
{Task t(sockfd, clientport, clientip);ThreadPool<Task>::GetInstance()->push(t);
}
Linux
客户端
- 实现一个翻译功能
#include <iostream> // 引入输入输出流库
#include <string> // 引入字符串处理库
#include <unistd.h> // 引入系统调用函数库,提供read/write等函数
#include <cstring> // 引入C字符串处理函数
#include <strings.h> // 引入处理字符串的辅助函数
#include <sys/socket.h> // 引入套接字相关函数
#include <sys/types.h> // 引入定义套接字数据类型
#include <netinet/in.h> // 引入网络协议族和结构体
#include <arpa/inet.h> // 引入IP地址转换函数#include "log.hpp" // 引入日志处理头文件// 打印程序的使用说明
void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port/ip[1024+]\n" << std::endl;
}int main(int argc, char *argv[])
{// 检查命令行参数个数if (argc != 3){Usage(argv[0]); // 参数不正确时输出使用说明return 1;}// 将命令行传入的端口号转为 uint16_t 类型,IP 地址存为字符串uint16_t port = std::stoi(argv[2]);std::string ip = argv[1];// 创建 sockaddr_in 结构体存储客户端的网络地址struct sockaddr_in client;unsigned int len = sizeof(client);bzero(&client, len); // 清空结构体,避免脏数据// 将 IP 地址从字符串转换为二进制格式并赋值给 client.sin_addrinet_pton(AF_INET, ip.c_str(), &(client.sin_addr));client.sin_family = AF_INET; // 设置地址族为 IPv4client.sin_port = htons(port); // 将端口号转换为网络字节序// 创建一个 TCP 套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){lg(FATAL, "Socket error. errno:%d ,error", errno, strerror(errno)); // 套接字创建失败时记录日志return 1;}// 连接到指定的服务器if (connect(sockfd, (struct sockaddr *)&client, sizeof(client)) < 0){lg(FATAL, "connect error. errno:%d ,error", errno, strerror(errno)); // 连接失败时记录日志return 1;}// 进入一个无限循环,不断接收用户输入并发送while (true){std::string message;// 提示用户输入消息std::cout << "Please Enter: ";getline(std::cin, message); // 获取用户输入// 发送消息给服务器int n = write(sockfd, message.c_str(), message.size());// 创建一个缓冲区接收服务器的回应char buffer[4096];ssize_t s = read(sockfd, &buffer, sizeof(buffer)); // 从服务器读取数据if (s > 0){buffer[s] = 0; // 在接收到的内容末尾加上字符串结束符std::cout << buffer << std::endl; // 输出服务器回应的内容}}return 0; // 程序正常结束
}
1. 命令行参数处理
- 程序接收两个命令行参数:服务器的 IP 地址和端口号。如果参数不正确,程序会输出使用说明并退出。
2. 创建套接字并连接服务器
- 使用
socket()
创建一个 TCP 套接字。 - 使用
connect()
函数连接到指定的服务器(通过 IP 地址和端口)。
3. 消息发送与接收
- 程序进入一个无限循环,不断等待用户输入消息。
- 用户输入的消息通过
write()
发送到服务器。 - 使用
read()
从服务器读取回显数据,并输出到屏幕。
4. 退出机制
- 该程序没有显式的退出机制,只有当程序被外部中断(如按
Ctrl+C
)时才会停止。
测试
Windows
客户端
#include <iostream>
#include <string>
#include <winsock2.h> // 包含 WinSock2 库,提供网络编程相关的函数
#pragma comment(lib, "ws2_32.lib") // 链接 Winsock 库#pragma warning(disable: 4996) // 禁用警告:编译器提醒使用不推荐的函数(如strcpy)using namespace std;int main() {// 设置控制台字符编码为 UTF-8SetConsoleOutputCP(CP_UTF8);// 初始化 WinSock 库WSADATA wsaData;int result = WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化 Winsock 库,要求 Winsock 版本为 2.2if (result != 0) {std::cerr << "WinSock初始化失败!错误代码:" << result << std::endl;return 1; // 如果初始化失败,则返回1}// 创建一个 TCP 套接字SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建一个 SOCK_STREAM 类型的套接字(TCP协议)if (clientSocket == INVALID_SOCKET) {std::cerr << "创建套接字失败!错误代码:" << WSAGetLastError() << std::endl;WSACleanup(); // 如果创建套接字失败,清理 Winsock 资源return 1; // 返回1表示程序失败}// 设置服务器的 IP 地址和端口sockaddr_in serverAddr;serverAddr.sin_family = AF_INET; // 地址族:IPv4serverAddr.sin_port = htons(8888); // 服务器端口,使用 htons 转换成网络字节序serverAddr.sin_addr.s_addr = inet_addr("119.3.219.187"); // 服务器 IP 地址// 连接到服务器result = connect(clientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)); // 发起连接请求if (result == SOCKET_ERROR) {std::cerr << "连接失败!错误代码:" << WSAGetLastError() << std::endl;closesocket(clientSocket); // 关闭套接字WSACleanup(); // 清理 Winsock 资源return 1; // 返回1表示程序失败}// 客户端主循环,持续发送消息和接收服务器响应while (true) {std::string message;getline(cin, message); // 从标准输入获取消息result = send(clientSocket, message.c_str(), message.size(), 0); // 发送消息到服务器if (result == SOCKET_ERROR) {std::cerr << "发送数据失败!错误代码:" << WSAGetLastError() << std::endl;closesocket(clientSocket); // 发送失败,关闭套接字WSACleanup(); // 清理 Winsock 资源return 1; // 返回1表示程序失败}char buffer[512]; // 用于接收服务器的响应数据result = recv(clientSocket, buffer, sizeof(buffer), 0); // 接收服务器响应if (result > 0) { // 如果接收成功buffer[result] = '\0'; // 确保数据结尾符(以便正确显示字符串)std::cout << buffer << std::endl; // 输出服务器响应}else if (result == 0) { // 如果返回 0,表示服务器关闭了连接std::cout << "服务器已关闭连接" << std::endl;break; // 跳出循环,结束客户端与服务器的通信}else { // 如果接收失败,输出错误信息std::cerr << "接收数据失败!错误代码:" << WSAGetLastError() << std::endl;}}// 关闭套接字并清理 WinSock 资源closesocket(clientSocket);WSACleanup(); // 清理 Winsock 资源return 0; // 正常结束
}
代码工作流程:
- 初始化 WinSock 库: 使用
WSAStartup
初始化 WinSock 库,指定要求使用的 WinSock 版本。 - 创建套接字: 创建一个 TCP 套接字,通过
socket
函数实现。 - 连接到服务器: 通过
connect
函数发起与指定 IP 地址和端口的连接。 - 发送和接收消息: 程序进入循环,等待用户输入消息,发送给服务器,并接收服务器的响应。
- 关闭连接: 当服务器关闭连接或发生错误时,客户端退出循环,关闭套接字并清理资源。