当前位置: 首页 > news >正文

深入理解 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)来完成:
    1. 一方(客户端或服务器)发送FIN(结束)包,表示没有数据发送了。
    2. 对方确认FIN包,发送ACK包。
    3. 对方也发送FIN包,表示关闭连接。
    4. 初始发送方确认对方的FIN包,并发送ACK包,连接关闭。

TCP socket接口

创建套接字(socket)

image-20250611125920793

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

image-20250611125923268

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数

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

监听连接 (listen)

image-20250611125926317

int listen(int sockfd, int backlog);

参数

  • sockfd:套接字描述符,用于标识你希望监听连接请求的套接字。

  • backlog:这个参数指定了待处理连接的队列的最大长度。也就是说,它定义了在处理接收到的连接请求之前,可以有多少个连接请求在等待队列中排队。

接受连接(accept)

image-20250611125929673

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() 后会被更新为实际存储的客户端地址长度。

数据传输readwrite

read

image-20250611125932725

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

image-20250611125935089

ssize_t write(int fd, const void *buf, size_t count);

参数:

  • fd: 文件描述符(file descriptor)。标识要写入的目标文件或设备。可以是由 open() 打开的文件,也可以是标准输出(1)、标准错误(2)等文件描述符。

  • buf: 一个指向数据缓冲区的指针,write() 会将缓冲区中的数据写入到指定的文件描述符中。buf 指向的数据是要写入的原始字节数据。

  • count: 要写入的字节数,即希望将多少字节的数据从 buf 写入到文件描述符 fd

关闭连接close

image-20250611125937715

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() 函数。
  • cstringstrings.h:用于处理字符串操作(如 memset()bzero())。
  • pthread.h:用于多线程操作,提供了线程创建、管理、同步等函数。
  • sys/socket.h:网络编程相关函数和数据类型。
  • netinet/in.harpa/inet.h:用于处理网络地址(IP 地址和端口)等。

2. 枚举类型 err

enum err
{SocketErr = 1,BindErr,ListenErr,
};

这个枚举类型定义了三种错误类型:SocketErrBindErrListenErr,分别对应套接字创建、绑定和监听过程中的错误。

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");
}
  1. 创建套接字:使用 socket() 创建一个 TCP 套接字。
  2. 设置套接字选项:使用 setsockopt() 设置套接字选项,允许端口复用(SO_REUSEADDR | SO_REUSEPORT)。
  3. 绑定套接字:使用 bind() 将套接字与指定的 IP 地址和端口绑定。
  4. 监听套接字:使用 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);}
}
  1. 等待客户端连接:使用 accept() 等待并接收客户端的连接。
  2. 获取客户端信息:获取客户端的 IP 和端口。
  3. 处理客户端请求:将客户端信息传递给线程池进行处理。
处理客户端请求方法 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)时才会停止。

测试

image-20250611125947275

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;  // 正常结束
}

代码工作流程:

  1. 初始化 WinSock 库: 使用 WSAStartup 初始化 WinSock 库,指定要求使用的 WinSock 版本。
  2. 创建套接字: 创建一个 TCP 套接字,通过 socket 函数实现。
  3. 连接到服务器: 通过 connect 函数发起与指定 IP 地址和端口的连接。
  4. 发送和接收消息: 程序进入循环,等待用户输入消息,发送给服务器,并接收服务器的响应。
  5. 关闭连接: 当服务器关闭连接或发生错误时,客户端退出循环,关闭套接字并清理资源。

测试:

image-20250611125951862

Linux源码

Windows源码

相关文章:

  • 数组方法_push()/pop()/数组方法_shift()/unshift()
  • 滚动—横向滚动时,如何直接滚动到对应的内容板块
  • `document.domain` API 的废弃与现代 Web 开发的转型
  • 从 8 秒到 1 秒:前端性能优化的 12 个关键操作
  • Maven 构建性能优化深度剖析:原理、策略与实践
  • CKA考试知识点分享(10)---NetworkPolicy
  • 深入浅出:C++深拷贝与浅拷贝
  • Web防火墙深度实战:从漏洞修补到CC攻击防御
  • 重拾前端基础知识:CSS预处理器
  • 基于AI智能体的医疗AI工具库构建路径分析
  • Python爬虫(54)Python数据治理全攻略:从爬虫清洗到NLP情感分析的实战演进
  • 第七章: SEO与渲染方式 三
  • C#接口代码记录
  • 第七章: SEO与渲染方式
  • Scrapy爬虫框架:数据采集的瑞士军刀(附实战避坑指南)!!!
  • ( github actions + workflow 01 ) 实现爬虫自动化,每2小时爬取一次澎湃新闻
  • MyBatis实战指南(七)MyBatis缓存机制
  • Python毕业设计226—基于python+爬虫+html的豆瓣影视数据可视化系统(源代码+数据库+万字论文)
  • Linux:多线程---线程控制(线程创建线程等待线程终止)
  • AJAX、Axios 与 Fetch:现代前端数据请求技术对比
  • 软件开发文档说明/应用商店关键词优化
  • 闵行网站建设公司纸/seo快排优化
  • 苏州网站开发公司电话/淘宝大数据查询平台
  • h5婚纱摄影网站模板/bt磁力王
  • 网站制作地点/江北seo
  • 成都招聘网站制作/石家庄seo结算