个人主页:chian-ocean
文章专栏-NET
深入理解 TCP 套接字:Socket 编程入门教程
- 前言
- TCP工作原理
- [TCP `socket`接口](#TCP
socket
接口) -
- 创建套接字(`socket`)
- 绑定地址(`bind`)
- 监听连接 (`listen`)
- 接受连接(`accept`)
- [**数据传输** (`read` 和`write` )](#数据传输 (
read
和write
)) - [**关闭连接** (`close`)](#关闭连接 (
close
))
- TCP`socket`编程
-
-
- [1. **包含的头文件**](#1. 包含的头文件)
- [2. **枚举类型 `err`**](#2. 枚举类型
err
) - [3. **`ThreadData` 类**](#3.
ThreadData
类) - [4. **`TcpServer` 类**](#4.
TcpServer
类) - [5. **多进程、多线程和线程池处理**](#5. 多进程、多线程和线程池处理)
-
- [**单进程**:直接调用 `Service()` 方法处理请求。](#单进程:直接调用
Service()
方法处理请求。) - [**多进程**:使用 `fork()` 创建子进程来处理每个客户端请求。](#多进程:使用
fork()
创建子进程来处理每个客户端请求。) - [**多线程**:使用 `pthread_create()` 创建线程来处理请求。](#多线程:使用
pthread_create()
创建线程来处理请求。) - **线程池**:通过线程池(`ThreadPool`)将客户端的处理任务添加到队列中,线程池会负责处理这些任务。
- [**单进程**:直接调用 `Service()` 方法处理请求。](#单进程:直接调用
-
- `Linux`客户端
-
-
- [1. **命令行参数处理**](#1. 命令行参数处理)
- [2. **创建套接字并连接服务器**](#2. 创建套接字并连接服务器)
- [3. **消息发送与接收**](#3. 消息发送与接收)
- [4. **退出机制**](#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
)

c
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
)

cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
: 这是一个已创建的套接字文件描述符,它是通过socket()
系统调用返回的。addr
: 指向一个sockaddr
结构体的指针,包含套接字所需绑定的地址信息。该结构体通常包括 IP 地址和端口号。addrlen
:addr
结构体的长度,以字节为单位。
监听连接 (listen
)

cpp
int listen(int sockfd, int backlog);
参数:
-
sockfd
:套接字描述符,用于标识你希望监听连接请求的套接字。 -
backlog
:这个参数指定了待处理连接的队列的最大长度。也就是说,它定义了在处理接收到的连接请求之前,可以有多少个连接请求在等待队列中排队。
接受连接(accept
)

cpp
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

cpp
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

cpp
ssize_t write(int fd, const void *buf, size_t count);
参数:
-
fd
: 文件描述符(file descriptor)。标识要写入的目标文件或设备。可以是由open()
打开的文件,也可以是标准输出(1
)、标准错误(2
)等文件描述符。 -
buf
: 一个指向数据缓冲区的指针,write()
会将缓冲区中的数据写入到指定的文件描述符中。buf
指向的数据是要写入的原始字节数据。 -
count
: 要写入的字节数,即希望将多少字节的数据从buf
写入到文件描述符fd
。
关闭连接 (close
)

cp
int close(int fd);
- 参数:
fd
是一个整型值,表示要关闭的文件描述符。
TCPsocket
编程
这个 C++ 代码实现了一个简单的 TCP 服务器,它可以通过多种方式处理客户端的请求:单进程、子进程、线程、线程池。下面我将对各个部分做详细的分析和注释。
1. 包含的头文件
cpp
#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
cpp
enum err
{
SocketErr = 1,
BindErr,
ListenErr,
};
这个枚举类型定义了三种错误类型:SocketErr
、BindErr
和 ListenErr
,分别对应套接字创建、绑定和监听过程中的错误。
3. ThreadData
类
cpp
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
类
这是整个服务器的核心类,负责初始化和管理服务器的套接字、绑定、监听以及处理客户端的请求。
成员变量
cpp
int sockfd_;
uint16_t port_;
std::string ip_;
sockfd_
:服务器套接字的文件描述符。port_
:服务器监听的端口。ip_
:服务器监听的 IP 地址。
构造函数和析构函数
cpp
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()
cpp
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));
// bind
struct 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()
cpp
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()
cpp
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()
方法处理请求。
cpp
// 单进程
void SingleProcess(int sockfd, const uint16_t &clientport, const std::string clientip)
{
Service(sockfd, clientport, clientip);
close(sockfd);
}
多进程 :使用 fork()
创建子进程来处理每个客户端请求。
cpp
// 多进程
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);
}
// father
close(sockfd);
int n = waitpid(id, nullptr, 0);
if (n > 0)
lg(INFO, "Child Process Quit...");
}
多线程 :使用 pthread_create()
创建线程来处理请求。
cpp
// 多线程
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
cpp
//线程池
void ThreadPolls(int sockfd, uint16_t &clientport,std::string clientip)
{
Task t(sockfd, clientport, clientip);
ThreadPool<Task>::GetInstance()->push(t);
}
Linux
客户端
- 实现一个翻译功能
cpp
#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_addr
inet_pton(AF_INET, ip.c_str(), &(client.sin_addr));
client.sin_family = AF_INET; // 设置地址族为 IPv4
client.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
客户端
cpp
#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-8
SetConsoleOutputCP(CP_UTF8);
// 初始化 WinSock 库
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化 Winsock 库,要求 Winsock 版本为 2.2
if (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; // 地址族:IPv4
serverAddr.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 地址和端口的连接。 - 发送和接收消息: 程序进入循环,等待用户输入消息,发送给服务器,并接收服务器的响应。
- 关闭连接: 当服务器关闭连接或发生错误时,客户端退出循环,关闭套接字并清理资源。
测试:
