TCP Socket 网络编程详解
本文详细讲解 TCP Socket 网络编程的核心 API,包括 socket、bind、listen、accept 和 connect 的用法。内容涵盖 TCP 三次握手与四次挥手原理、客户端与服务端代码实现、单进程及多进程/多线程模型对比,以及线程池的应用。此外,还介绍了守护进程的创建步骤、信号处理(如 SIGPIPE)及标准输出重定向,帮助开发者构建稳定运行的后台网络服务。
Socket 类型与参数
套接字域 (Domain)
第一个参数指定协议家族,例如 AF_INET 表示 IPv4,AF_INET6 表示 IPv6,AF_UNIX 表示本地通信。
套接字类型 (Type)
第二个参数定义套接字类型:
SOCK_STREAM: 面向字节流,对应 TCP。SOCK_DGRAM: 面向用户数据报,对应 UDP。
协议 (Protocol)
第三个参数通常填 0,系统会根据前两个参数自动选择默认协议。
TCP Socket API 详解
以下函数都在 <sys/socket.h> 中定义。
socket()
打开一个网络通讯端口。如果成功,返回文件描述符;如果出错,返回 -1。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
对于 IPv4,family 参数指定为 AF_INET;对于 TCP 协议,type 参数指定为 SOCK_STREAM。
bind()
服务器程序需要调用 bind 绑定固定的网络地址和端口号。
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
inet_aton(ip_.c_str(), &(local.sin_addr));
bind(sockfd_, (struct sockaddr *)(&local), sizeof(local));
- 将 sockfd 和 myaddr 绑定在一起。
- 成功返回 0,失败返回 -1。
- 注意:云服务器公网 IP 通常不能直接绑定,一般绑定
0.0.0.0或内网 IP。
listen()
声明 sockfd 处于监听状态,并设置等待队列长度 backlog。
listen(sockfd_, backlog);
- 最多允许 backlog 个客户端处于连接等待状态。
- 成功返回 0,失败返回 -1。
accept()
三次握手完成后,服务器调用 accept() 接受连接。
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sockfd = accept(listensockfd_, (struct sockaddr*)(&client), &len);
- 如果没有客户端连接请求,会阻塞等待。
- 返回新的文件描述符用于通信,原监听描述符保持不变。
- 类似于'拉客',主线程继续监听,新连接由新描述符处理。
connect()
客户端需要调用 connect() 连接服务器。
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
connect(sockfd, (struct sockaddr*)&server, sizeof(server));
- 客户端发起 connect 时,系统会自动随机 bind 一个端口。
- 必须知道服务器的 IP 和端口。
服务端与客户端代码实现
服务端基础结构
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
const int defaultfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10;
enum { UsageError = 1, SocketError, BindError, ListenError };
class TcpServer {
public:
TcpServer(const uint16_t &port, const string &ip = defaultip)
: listensockfd_(defaultfd), port_(port), ip_(ip) {}
void InitServer() {
// 创建套接字
listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (listensockfd_ < 0) {
perror("create socket error");
exit(SocketError);
}
// 初始化本地信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
inet_aton(ip_.c_str(), &(local.sin_addr));
// 绑定
int k = bind(listensockfd_, (struct sockaddr *)(&local), sizeof(local));
if (k < 0) {
perror("bind error");
exit(BindError);
}
// 监听
int ls = listen(listensockfd_, backlog);
if (ls < 0) {
perror("listen error");
exit(ListenError);
}
}
void Start() {
for (;;) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensockfd_, (struct sockaddr*)(&client), &len);
if (sockfd < 0) continue;
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
printf("get a new link..., sockfd:%d, client ip:%s, client port:%d\n",
sockfd, clientip, clientport);
// 此处应启动线程或进程处理 sockfd
}
}
private:
int listensockfd_;
uint16_t port_;
string ip_;
};
客户端基础结构
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
int main(int argc, char* argv[]) {
if(argc != 3) {
cerr << "Usage: ./tcpclient serverip serverport" << endl;
return 1;
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0) {
cerr << "socket error" << endl;
return 1;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
if(n < 0) {
cerr << "connect error" << endl;
return 2;
}
string message;
while(true) {
cout << "Please Enter#";
getline(cin, message);
write(sockfd, message.c_str(), message.size());
char inbuffer[4096];
int read_n = read(sockfd, inbuffer, sizeof(inbuffer));
if(read_n > 0) {
inbuffer[read_n] = 0;
cout << inbuffer << endl;
}
}
close(sockfd);
return 0;
}
并发模型演进
单进程版本
- 缺点:只允许一个用户在线,客户端断开后无法处理其他请求。
多进程版本
- 使用
fork()创建子进程处理连接。 - 缺点:消耗资源高,成本太高。
多线程版本
- 使用
pthread_create()创建线程处理连接。 - 优点:比多进程更轻量。
- 注意:需使用
pthread_detach分离线程,避免资源泄漏。
线程池版本
- 使用线程池管理固定数量的工作线程。
- 优点:控制资源消耗,提高性能。
- 实现:任务类继承
operator(),放入线程池队列。
守护进程与信号处理
前台与后台进程
- 前台进程受终端影响,关闭终端可能终止进程。
- 后台进程独立运行,不受终端关闭影响。
守护进程化步骤
- 忽略异常信号:如
SIGCLD,SIGPIPE,SIGSTOP。 - 创建新会话:调用
setsid()脱离控制终端。 - 更改目录:切换到根目录
/。 - 重定向标准 IO:将 stdin/stdout/stderr 重定向到
/dev/null或日志文件。
信号处理
常见信号及其含义:
SIGHUP: 终端挂断,守护进程通常忽略。SIGINT: Ctrl+C 中断,守护进程通常忽略。SIGTERM: 终止请求,可捕获进行清理。SIGQUIT: 退出并生成 core dump,通常忽略。SIGCHLD: 子进程结束,用于回收僵尸进程。SIGPIPE: 管道破裂,写入已关闭的套接字时触发,建议忽略。
示例代码:
void Daemon(const string &cwd = "") {
signal(SIGCLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
if(fork() > 0) exit(0);
setsid();
if(!cwd.empty()) chdir(cwd.c_str());
int fd = open("/dev/null", O_RDWR);
dup2(fd, 0); dup2(fd, 1); dup2(fd, 2);
close(fd);
}
测试验证
- 检查端口:
netstat -nltp - 查看进程:
ps ajx | grep tcpserver - 验证后台运行:关闭终端后进程仍在运行。
总结
TCP 通信是全双工的。通过合理设计 Socket API 调用流程,结合多线程或线程池模型,并配置为守护进程运行,可以构建稳定、高效的网络服务器。


