前言: 继上一篇完成了 UDP 协议的复习后,最近梳理了 TCP 协议的底层实现。与 UDP "即发即忘"的特性不同,TCP 作为一种面向连接、可靠的字节流协议,虽然握手和挥手的过程增加了复杂性,但它是构建稳定网络服务(如 HTTP、RPC)的基石。
一、 TCP Socket 编程的核心
在Linux中,"一切皆文件"。网络通信本质上也是一种对文件的读写操作。要建立一个TCP连接,客户端和服务器必须遵循特定的"握手"流程。
核心API一览
TCP编程的核心函数都在 <sys/socket.h> 中
socket() : 创建通信端点
类似于文件操作的 open(),成功返回文件描述符,失败返回-1 。
关键参数 为 AF_INET (IPv4), SOCK_STREAM (TCP面向流) 。
bind() : 绑定IP和端口
服务器必须绑定固定的IP和端口,这样客户端才能找得到它 ,如果不显式绑定,内核会随机分配,这对服务器是不可接受的。
listen() : 设置监听状态
该函数的功能是告诉内核:"我准备好了,可以接收连接请求了"。其中 backlog 参数:指定连接等待队列的长度(一般设为5-10),处理不过来的连接会在这里排队 。
accept() : 获取连接
accept 返回的是一个新的文件描述符 (我们可以叫它 connfd),专门用于和当前这个客户端进行通信。而原本的 listenfd 继续监听新的连接。这就像饭店门口的迎宾小姐(listenfd)只负责把人领进来,具体服务由服务员(connfd)负责 。
connect() : 发起连接(客户端)
客户端调用此函数向服务器发起三次握手 。
上述API的使用将在下面的例子中详细介绍。
二、不同版本的TCP服务器
V1版本:单进程循环服务器
核心代码逻辑
单进程的服务器初始化时,我们需要填充 sockaddr_in 结构体:
cpp
// 初始化服务器地址结构
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清零
local.sin_family = AF_INET; // 地址类型
local.sin_port = htons(_port); // 端口号转网络字节序
// INADDR_ANY: 绑定本机所有IP,适合多网卡机器
local.sin_addr.s_addr = htonl(INADDR_ANY);
// 1. Socket -> 2. Bind -> 3. Listen
socket(...);
bind(...);
listen(...);
在 Start() 启动循环中:
cpp
while (true) {
// 4. 获取连接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// accept 是阻塞的,直到有客户端连上来 [cite: 67]
int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sockfd < 0) continue;
// 5. 提供服务
Service(sockfd); // 执行读写循环
close(sockfd); // 服务结束关闭连接
}
为什么单进程是不科学的?
在 V1 版本中,如果 Service(sockfd) 内部是一个 while(true) 循环(为了持续和同一个客户端聊天),那么主程序的 while 循环就会卡在 Service 里,无法返回去调用 accept 。
结果:只要第一个客户端不退,第二个客户端就永远连不上,处于阻塞状态。这显然不符合服务器"并发"的要求 。
单进程TcpServer.hpp源码如下:
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "Logger.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
static const uint16_t gdefaultsockfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;
using task_t = std::function<void()>;
class TcpEchoServer
{
private:
// 长任务 -> sockfd -> 长链接 不太适合线程池技术
void Service(int sockfd, InetAddr client)
{
char buffer[1024];
// ⼀直进⾏IO
while (true)
{
buffer[0] = 0;
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
LOG(LogLevel::INFO) << "client [" << client.ToString() << "] say# " << buffer;
std::string echo_string = "server echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0) // read如果返回值是0,表⽰读到了⽂件结尾(对端关闭了连接!)
{
LOG(LogLevel::INFO) << "client " << client.ToString() << " quit, close sockfd: " << sockfd;
break;
}
else
{
LOG(LogLevel::WARNING) << "read client " << client.ToString() << " error, sockfd: " << sockfd;
break;
}
}
close(sockfd);
}
public:
TcpEchoServer(uint16_t port = gport) : _listensockfd(gdefaultsockfd), _port(port)
{
}
void Init()
{
// 1.创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // tcp
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create tcp socket error";
exit(SOCK_CREATE_ERROR);
}
LOG(LogLevel::INFO) << "create tcp socket success: " << _listensockfd;
// 2.bind套接字
InetAddr addr(_port);
if (bind(_listensockfd, addr.Addr(), addr.Length()) != 0)
{
LOG(LogLevel::FATAL) << "bind socker error";
exit(SOCK_BIND_ERROR);
}
LOG(LogLevel::INFO) << "bind socker success: " << _listensockfd;
// 3.listen套接字
if (listen(_listensockfd, gbacklog) != 0) // tcp
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(SOCK_LISTEN_ERROR);
}
LOG(LogLevel::INFO) << "listen socket success: " << _listensockfd;
}
void Start()
{
// signal(SIGCHLD, SIG_IGN); // 防止父进程一直阻塞等待
while (true)
{
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(peer);
int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len); // 默认阻塞
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept client error";
continue;
}
InetAddr clientaddr(peer);
LOG(LogLevel::INFO) << "获取新链接成功, sockfd is : " << sockfd << "client addr: " << clientaddr.ToString();
// 1. 单进程处理
Service(sockfd,clientaddr); // 单进程
}
}
~TcpEchoServer()
{
}
private:
int _listensockfd;
int _port;
};
V2版本:多进程并发
为了让服务器能同时服务多个人,利用 fork() 创建子进程是经典的Unix解法。
核心思想
- 父进程只负责
accept获取连接。- 一旦获取连接,
fork一个子进程。- 子进程 继承了父进程的文件描述符,负责运行
Service(sockfd)。- 父进程 关闭
sockfd(因为它不需要通信),继续回去accept。
僵尸进程的处理
多进程编程必须处理子进程退出后的资源回收,否则会产生僵尸进程。使用一种巧妙的双重Fork(孤儿进程) 技巧:
cpp
pid_t id = fork();
if (id == 0) { // Child
close(_listensock); // 子进程不需要监听socket
if (fork() > 0) exit(0); // 孙子进程被创建,儿子进程直接退出
// 这里是孙子进程,由于父进程(儿子进程)已死,它被系统领养(init/systemd)
// 系统会自动回收它的资源,无需我们就操心waitpid了
Service(sockfd);
close(sockfd);
exit(0);
}
// Father
close(sockfd); // 父进程切记关闭sockfd,否则文件描述符泄露
waitpid(id, nullptr, 0); // 回收儿子进程(因为儿子进程立刻退出了,不会阻塞很久)
多进程TcpServer.hpp源码如下:
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "Logger.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
static const uint16_t gdefaultsockfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;
using task_t = std::function<void()>;
class TcpEchoServer
{
private:
// 长任务 -> sockfd -> 长链接 不太适合线程池技术
void Service(int sockfd, InetAddr client)
{
char buffer[1024];
// ⼀直进⾏IO
while (true)
{
buffer[0] = 0;
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
LOG(LogLevel::INFO) << "client [" << client.ToString() << "] say# " << buffer;
std::string echo_string = "server echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0) // read如果返回值是0,表⽰读到了⽂件结尾(对端关闭了连接!)
{
LOG(LogLevel::INFO) << "client " << client.ToString() << " quit, close sockfd: " << sockfd;
break;
}
else
{
LOG(LogLevel::WARNING) << "read client " << client.ToString() << " error, sockfd: " << sockfd;
break;
}
}
close(sockfd);
}
public:
TcpEchoServer(uint16_t port = gport) : _listensockfd(gdefaultsockfd), _port(port)
{
}
void Init()
{
// 1.创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // tcp
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create tcp socket error";
exit(SOCK_CREATE_ERROR);
}
LOG(LogLevel::INFO) << "create tcp socket success: " << _listensockfd;
// 2.bind套接字
InetAddr addr(_port);
if (bind(_listensockfd, addr.Addr(), addr.Length()) != 0)
{
LOG(LogLevel::FATAL) << "bind socker error";
exit(SOCK_BIND_ERROR);
}
LOG(LogLevel::INFO) << "bind socker success: " << _listensockfd;
// 3.listen套接字
if (listen(_listensockfd, gbacklog) != 0) // tcp
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(SOCK_LISTEN_ERROR);
}
LOG(LogLevel::INFO) << "listen socket success: " << _listensockfd;
}
void Start()
{
signal(SIGCHLD, SIG_IGN); // 防止父进程一直阻塞等待
while (true)
{
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(peer);
int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len); // 默认阻塞
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept client error";
continue;
}
InetAddr clientaddr(peer);
LOG(LogLevel::INFO) << "获取新链接成功, sockfd is : " << sockfd << "client addr: " << clientaddr.ToString();
// 2.多进程处理
pid_t id = fork();
if (id < 0)
{
LOG(LogLevel::FATAL) << "资源不足, 创建子进程失败";
exit(FORK_ERROR);
}
else if (id == 0)
{
// 子进程
close(_listensockfd); // 防御性编程
if(fork() > 0)
exit(OK);
// 孙子进程
Service(sockfd, clientaddr);
exit(OK);
}
else
{
close(sockfd); // 1. 关闭无用fd 2. 规避fd泄漏(可用fd越来越少)
pid_t rid = waitpid(id, nullptr, 0);
(void)rid;
}
}
}
~TcpEchoServer()
{
}
private:
int _listensockfd;
int _port;
};
V3版本:多线程轻量级并发
进程创建的开销比较大,且进程间通信复杂。多线程是更现代的写法。
遇到的坑:参数传递
我们使用 pthread_create 启动线程,但线程函数 static void *Routine(void args) 只能接收一个 void 参数。 我们需要传递 sockfd,最好还有客户端的 sockaddr_in 信息。
我们可以定义一个 ThreadData 类来封装这些参数 。
cpp
class ThreadData {
public:
int _sockfd;
InetAddr _addr; // 封装了IP和Port的类
ThreadData(int sockfd, struct sockaddr_in addr) : _sockfd(sockfd), _addr(addr) {}
};
线程分离
原因在于主线程不希望阻塞在 pthread_join 上等待子线程结束。
解决方案为在线程函数内部调用 pthread_detach(pthread_self())。这样线程结束后,资源自动由OS回收 。
cpp
static void *Routine(void *arg)
{
ThreadData *td = static_cast<ThreadData *>(arg);
pthread_detach(pthread_self()); // 线程分离
td->_self->Service(td->_sockfd, td->_client);
delete td;
return nullptr;
}
多线程TcpServer.hpp源码如下:
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "Logger.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
static const uint16_t gdefaultsockfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;
using task_t = std::function<void()>;
class TcpEchoServer
{
private:
// 长任务 -> sockfd -> 长链接 不太适合线程池技术
void Service(int sockfd, InetAddr client)
{
char buffer[1024];
// ⼀直进⾏IO
while (true)
{
buffer[0] = 0;
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
LOG(LogLevel::INFO) << "client [" << client.ToString() << "] say# " << buffer;
std::string echo_string = "server echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0) // read如果返回值是0,表⽰读到了⽂件结尾(对端关闭了连接!)
{
LOG(LogLevel::INFO) << "client " << client.ToString() << " quit, close sockfd: " << sockfd;
break;
}
else
{
LOG(LogLevel::WARNING) << "read client " << client.ToString() << " error, sockfd: " << sockfd;
break;
}
}
close(sockfd);
}
public:
TcpEchoServer(uint16_t port = gport) : _listensockfd(gdefaultsockfd), _port(port)
{
}
void Init()
{
// 1.创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // tcp
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create tcp socket error";
exit(SOCK_CREATE_ERROR);
}
LOG(LogLevel::INFO) << "create tcp socket success: " << _listensockfd;
// 2.bind套接字
InetAddr addr(_port);
if (bind(_listensockfd, addr.Addr(), addr.Length()) != 0)
{
LOG(LogLevel::FATAL) << "bind socker error";
exit(SOCK_BIND_ERROR);
}
LOG(LogLevel::INFO) << "bind socker success: " << _listensockfd;
// 3.listen套接字
if (listen(_listensockfd, gbacklog) != 0) // tcp
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(SOCK_LISTEN_ERROR);
}
LOG(LogLevel::INFO) << "listen socket success: " << _listensockfd;
}
static void *Routine(void *arg)
{
ThreadData *td = static_cast<ThreadData *>(arg);
pthread_detach(pthread_self()); // 线程分离
td->_self->Service(td->_sockfd, td->_client);
delete td;
return (void*)1;
}
class ThreadData
{
public:
int _sockfd;
TcpEchoServer *_self;
InetAddr _client;
ThreadData(int sockfd, TcpEchoServer *self, InetAddr client) : _sockfd(_sockfd), _self(self), _client(client)
{
}
};
void Start()
{
while (true)
{
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(peer);
int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len); // 默认阻塞
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept client error";
continue;
}
InetAddr clientaddr(peer);
LOG(LogLevel::INFO) << "获取新链接成功, sockfd is : " << sockfd << "client addr: " << clientaddr.ToString();
// 3.多线程处理
pthread_t tid;
ThreadData *td = new ThreadData(sockfd, this, clientaddr);
pthread_create(&tid, nullptr, Routine, (void *)td);
}
}
~TcpEchoServer()
{
}
private:
int _listensockfd;
int _port;
};
V4版本:线程池优化
虽然V3版本解决了并发,但如果有海量短连接(比如几万个请求瞬间涌入),频繁创建和销毁线程会极大地消耗CPU资源。
核心逻辑
- 预先创建好一批线程(Worker Threads)。
- 主线程
accept成功后,不再创建新线程,而是将Service任务封装成一个Task(可以使用std::bind适配器)。- 将任务
Push到线程池的任务队列中。- 空闲的线程抢占任务并执行 。
cpp
// 伪代码示例
using func_t = std::function<void()>;
// 将Service函数和参数绑定,变成无参的函数对象
func_t func = std::bind(&TcpServer::Service, this, sockfd, addr);
// 扔进线程池
ThreadPool<func_t>::GetInstance()->Enqueue(func);
线程池TcpServer.hpp源码如下:
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "Logger.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
static const uint16_t gdefaultsockfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;
using task_t = std::function<void()>;
class TcpEchoServer
{
private:
// 长任务 -> sockfd -> 长链接 不太适合线程池技术
void Service(int sockfd, InetAddr client)
{
char buffer[1024];
// ⼀直进⾏IO
while (true)
{
buffer[0] = 0;
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
LOG(LogLevel::INFO) << "client [" << client.ToString() << "] say# " << buffer;
std::string echo_string = "server echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0) // read如果返回值是0,表⽰读到了⽂件结尾(对端关闭了连接!)
{
LOG(LogLevel::INFO) << "client " << client.ToString() << " quit, close sockfd: " << sockfd;
break;
}
else
{
LOG(LogLevel::WARNING) << "read client " << client.ToString() << " error, sockfd: " << sockfd;
break;
}
}
close(sockfd);
}
public:
TcpEchoServer(uint16_t port = gport) : _listensockfd(gdefaultsockfd), _port(port)
{
}
void Init()
{
// 1.创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // tcp
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create tcp socket error";
exit(SOCK_CREATE_ERROR);
}
LOG(LogLevel::INFO) << "create tcp socket success: " << _listensockfd;
// 2.bind套接字
InetAddr addr(_port);
if (bind(_listensockfd, addr.Addr(), addr.Length()) != 0)
{
LOG(LogLevel::FATAL) << "bind socker error";
exit(SOCK_BIND_ERROR);
}
LOG(LogLevel::INFO) << "bind socker success: " << _listensockfd;
// 3.listen套接字
if (listen(_listensockfd, gbacklog) != 0) // tcp
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(SOCK_LISTEN_ERROR);
}
LOG(LogLevel::INFO) << "listen socket success: " << _listensockfd;
}
void Start()
{
// signal(SIGCHLD, SIG_IGN); // 防止父进程一直阻塞等待
while (true)
{
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(peer);
int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len); // 默认阻塞
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept client error";
continue;
}
InetAddr clientaddr(peer);
LOG(LogLevel::INFO) << "获取新链接成功, sockfd is : " << sockfd << "client addr: " << clientaddr.ToString();
// 多进程 多线程
// 1.效率问题 connect成功才创建进程线程
// 2.执行流个数没有上限
// 4.线程池
ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, clientaddr]()
{ this->Service(sockfd, clientaddr); });
}
}
~TcpEchoServer()
{
}
private:
int _listensockfd;
int _port;
};
根据如上几个版本,使用TcpServer.cc进行服务器构建:
cpp
#include <memory>
#include "TcpServer.hpp"
void Usage(std::string proc)
{
std::cerr << "Usage : " << proc << " serverport" << std::endl;
}
// ./tcp_server serverport
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
//EnableConsoleLogStrategy(); // 启动日志
std::unique_ptr<TcpEchoServer> ptr = std::make_unique<TcpEchoServer>();
ptr->Init();
ptr->Start();
return 0;
}
三、客户端的实现细节
客户端相对简单,TcpClient.cc源码如下:
cpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include "Comm.hpp"
#include "InetAddr.hpp"
void Usage(std::string proc)
{
std::cerr << "Usage : " << proc << " serverip serverport" << std::endl;
}
// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "create client sockfd error" << std::endl;
exit(SOCK_CREATE_ERROR);
}
InetAddr server(serverport, serverip);
if (connect(sockfd, server.Addr(), server.Length()) != 0)
{
std::cerr << "connect sockfd error" << std::endl;
exit(SOCK_CONNECT_ERROR);
}
std::cout << "connect " << server.ToString() << " success" << std::endl;
while (true)
{
std::cout << "Please Enter@ ";
std::string line;
std::getline(std::cin, line);
ssize_t n = write(sockfd, line.c_str(), line.size());
if(n >= 0)
{
char buffer[1024];
buffer[0] = 0;
ssize_t m = read(sockfd,buffer,sizeof(buffer));
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
}
return 0;
}
客户端需要bind吗?
答案是当然需要bind,但不需要也不建议用户显式调用 bind。
原因 在于如果客户端强行绑定 9999 端口,那么这台机器上就只能启动一个客户端实例了(端口冲突)。在调用 connect() 时,如果OS发现socket没有绑定,会自动为它分配一个临时的、唯一的端口号 。
四、总结与思考
| 版本 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| V1 | 单进程循环 | 简单易懂 | 无法并发,一次只能服务一人 |
| V2 | 多进程 (Fork) | 隔离性好,健壮 | 进程创建销毁开销大,上下文切换慢 |
| V3 | 多线程 | 轻量,并发度高 | 线程不安全,单线程崩溃可能导致整个进程挂掉 |
| V4 | 线程池 | 资源复用,性能稳 | 实现复杂度稍高,需配合任务队列 |
重点细节在于
- 务必理解
INADDR_ANY的作用 。 - 注意
read返回值为0表示对端关闭连接(EOF),这是TCP连接断开的重要标志 。 - 如果不熟悉
htons/htonl,请复习网络字节序与主机字节序的大小端问题。
希望这篇笔记能帮你打通Linux TCP编程的任督二脉!