释放小七猫的分享欲

Unix 上的 TCP-Socket 编程

categories: 技术 tags: 软件
@ 16/08/2025

TCP 套接字 (Socket) 编程基础知识

TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。绝大多数的计算机系统都将运输层以下的网络协议在操作系统的内核中进行实现。应用程序想执行网络操作,必须通过操作系统为应用程序提供的接口,这个接口被称为网络应用编程接口(Application Programming Interface,API)。每一个操作系统都可以自由地定义自己的网络 API,其中 Socket API 有着广泛的支持,几乎所有主流的操作系统都支持它。Socket API 定义了一组数据结构和操作。应用程序中进行网络操作之前必须先调用 Soctet API 中定义的操作,创建 Socket 数据结构,以获得所需要的资源。由于 Socket 数据结构位于操作系统内核,因此创建返回的是 Socket 描述符来操作该 Socket 数据结构。此后,应用程序所进行的网络操作(建立连接、收发数据、调整网络通信参数等)都必须以该描述符为参数调用 Socket API 来完成。

下面在 macOS 上演示说明如何使用 Soctet API 编写简单的 TCP 应用。

套接字 API 的基本流程

  1. 创建套接字

    int socket(int domain, int type, int protocol);
    
    • domain: 协议域,如 AF_INET 表示 IPv4
    • type: 套接字类型,如 SOCK_STREAM 表示 TCP
    • protocol: 协议,通常为 0 表示默认
  2. 服务器端操作 服务器需要执行以下三个操作:

    int bind(int socket, struct sockaddr* address, int addr_len);
    int listen(int socket, int backlog);
    int accept(int socket, struct sockaddr* address, int *addr_len);
    
    • bind: 将套接字绑定到指定地址和端口
    • listen: 开始监听客户端连接请求
    • accept: 接受客户端连接请求
  3. 客户端操作 客户端执行主动打开操作:

    int connect(int socket, struct sockaddr* address, int addr_len);
    
    • connect: 向服务器发起连接请求
  4. 数据传输 建立连接后,应用程序可以调用以下操作发送和接收数据:

    int send(int socket, char *message, int msg_len, int flags);
    int recv(int socket, char *message, int *buffer, int buf_len, int flags);
    
    • send: 发送数据
    • recv: 接收数据
  5. 关闭套接字 最后需要关闭套接字以释放资源:

    int close(int socket);
    

环境准备

在 macOS 系统下进行 TCP 套接字编程,需要使用 C++ 编译器(如 g++)和系统提供的网络编程头文件。主要包含以下头文件:

  • sys/socket.h:套接字相关函数
  • netinet/in.h:网络地址结构定义
  • arpa/inet.h:IP 地址转换函数
  • unistd.h:系统调用函数
  • stdio.h:标准输入输出函数
  • string.h:字符串处理函数

服务器端代码

服务器端程序负责监听客户端连接请求,并与客户端进行数据交互。

#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;

#define SERVER_PORT  5050               //端口号
#define SERVER_IP    "127.0.0.1"    //服务器 ip
#define QUEUE_SIZE   5                  //所监听端口队列大小

int main(int argc, char *argv[])
{
    //创建一个套接字,并检测是否创建成功
    int sockSer;
    sockSer = socket(AF_INET, SOCK_STREAM, 0);
    if(sockSer == -1){
        perror("socket");
    }

    //设置端口可以重用,可以多个客户端连接同一个端口,并检测是否设置成功
    int yes = 1;
    if(setsockopt(sockSer, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1){
        perror("setsockopt");
    }

    struct sockaddr_in addrSer,addrCli;        //创建一个记录地址信息的结构体
    addrSer.sin_family = AF_INET;              //所使用 AF_INET 协议族
    addrSer.sin_port = htons(SERVER_PORT);     //设置地址结构体中的端口号
    addrSer.sin_addr.s_addr = inet_addr(SERVER_IP);   //设置其中的服务器 ip

    //将套接字地址与所创建的套接字号联系起来。并检测是否绑定成功
    socklen_t addrlen = sizeof(struct sockaddr);
    int res = ::bind(sockSer,(struct sockaddr*)&addrSer,addrlen);
    if(res == -1)
        perror("bind");

    listen(sockSer, QUEUE_SIZE);       //监听端口队列是否由连接请求,如果有就将该端口设置位可连接状态,等待服务器接收连接

    printf("Server Wait Client Accept......\n");
    //如果监听到有连接请求接受连接请求。并检测是否连接成功,成功返回 0,否则返回 -1
    int sockConn = accept(sockSer, (struct sockaddr*)&addrCli, &addrlen);
    if(sockConn == -1)
        perror("accept");
    else
    {
        printf("Server Accept Client OK.\n");
        printf("Client IP:> %s\n", inet_ntoa(addrCli.sin_addr));
        printf("Client Port:> %d\n",ntohs(addrCli.sin_port));
    }

    char sendbuf[256];         //申请一个发送缓存区
    char recvbuf[256];         //申请一个接收缓存区
    while(1)
    {
        // 先显示服务器提示符,再等待用户输入
        printf("Ser:>");
        // 使用 fgets 读取整行输入,包括空格
        if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL) {
            printf("读取输入失败\n");
            break;
        }
        // 移除换行符
        sendbuf[strcspn(sendbuf, "\n")] = 0;

        if(strncmp(sendbuf,"quit",4) == 0)    //如果所要发送的数据为"quit",则直接退出。
            break;

        // 发送数据给客户端
        if (send(sockConn, sendbuf, strlen(sendbuf)+1, 0) <= 0) {
            printf("发送数据失败\n");
            break;
        }

        // 接收客户端发送的数据
        memset(recvbuf, 0, sizeof(recvbuf));
        int recv_len = recv(sockConn, recvbuf, sizeof(recvbuf)-1, 0);
        if (recv_len > 0) {
            printf("Cli:> %s\n",recvbuf);
        } else if (recv_len == 0) {
            printf("客户端已断开连接\n");
            break;
        } else {
            perror("recv");
            break;
        }
    }
    close(sockSer);         //关闭套接字
    return 0;
}

客户端代码

客户端程序负责连接到服务器,并与服务器进行数据交互。

#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;

#define SERVER_PORT  5050
#define SERVER_IP    "127.0.0.1"

int main(int argc, char *argv[])
{
    //创建客户端套接字号,并检测是否创建成功
    int sockCli;
    sockCli = socket(AF_INET, SOCK_STREAM, 0);
    if(sockCli == -1)
        perror("socket");

    //创建一个地址信息结构体,并对其内容进行设置
    struct sockaddr_in addrSer,addrCli;
    addrSer.sin_family = AF_INET;         //使用 AF_INET 协议族
    addrSer.sin_port = htons(SERVER_PORT);  //设置端口号
    addrSer.sin_addr.s_addr = inet_addr(SERVER_IP);   //设置服务器 ip

    bind(sockCli,(struct sockaddr*)&addrCli, sizeof(struct sockaddr));    //将套接字地址与所创建的套接字号联系起来

    //创建一个与服务器的连接,并检测连接是否成功
    socklen_t addrlen = sizeof(struct sockaddr);
    int res = connect(sockCli,(struct sockaddr*)&addrSer, addrlen);
    if(res == -1)
        perror("connect");
    else
        printf("Client Connect Server OK.\n");

    char sendbuf[256];     //申请一个发送数据缓存区
    char recvbuf[256];     //申请一个接收数据缓存区
    while(1)
    {
        // 先显示客户端提示符,再等待用户输入
        printf("Cli:>");
        // 使用 fgets 读取整行输入,包括空格
        if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL) {
            printf("读取输入失败\n");
            break;
        }
        // 移除换行符
        sendbuf[strcspn(sendbuf, "\n")] = 0;

        if(strncmp(sendbuf,"quit", 4) == 0)    //如果客户端发送的数据为"quit",则退出。
            break;

        send(sockCli, sendbuf, strlen(sendbuf)+1, 0);   //发送数据

        // 接收来自服务器的数据
        memset(recvbuf, 0, sizeof(recvbuf));
        int recv_len = recv(sockCli, recvbuf, sizeof(recvbuf)-1, 0);
        if (recv_len > 0) {
            printf("Ser:> %s\n",recvbuf);
        } else if (recv_len == 0) {
            printf("服务器已断开连接\n");
            break;
        } else {
            perror("recv");
            break;
        }
    }
    close(sockCli);       //关闭套接字
    return 0;
}

编译和运行

在 macOS 系统下,可以使用以下命令编译这两个程序:

g++ -o TcpServer Tcpserver.cpp
g++ -o TcpClient TcpClient.cpp

然后分别运行服务器和客户端程序:

./TcpServer
./TcpClient

运行 ./TcpServer

Documents/tmp$ ./TcpServer
Server Wait Client Accept......
Server Accept Client OK.
Client IP:> 127.0.0.1
Client Port:> 54109
Ser:>

运行 ./TcpClient

:~/Documents/tmp$ ./TcpClient
Client Connect Server OK.
Cli:>

然后就可以进行简单的交互了: