Socket网络编程——TCP协议
本文最后更新于 32 天前,其中的信息可能已经有所发展或是发生改变。

简单概念

套接字Socket

简单通俗来说,Socket 就是 Linux 系统里,用来实现网络通信的“工具”或“端点”,可以想象成“两部电话之间的电话线+电话机”。有了 Socket,A电脑才能发数据给B电脑,B电脑才能收到数据。

标准来说,Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。

在 Linux 里,Socket 本质就是一个文件描述符(fd),和文件、串口、管道一样,用read/write/close操作。

Socket = {IP地址,端口号},一次网络通信必须要两个Socket:客户端socket与服务端socket,它们一一对应,双向通信。

TCP协议

TCP(Transmission Control Protocol 传输控制协议),具有以下三大特点:

  1. 面向连接:通信前必须先建立连接(三次握手)
  2. 可靠传输:不丢包、不乱序、出错重传
  3. 基于字节流:数据像水流一样连续,无边界→会产生粘包

TCP连接的生命周期:

  1. 三次握手→建立连接
  2. 数据传输→send(发送)/recv(接收)
  3. 四次挥手→断开连接

三次握手

目的:确保客户端和服务端的发送、接收能力都正常。TCP 连接在三次握手完成后才建立。

四次挥手

目的:安全关闭连接,保证数据收发完毕。

主动关闭连接的一方会进入TIME_WAIT持续 2MSL 作用:确保对方收到最后的ACK


核心流程

讲完了定义概念等方面,接下来我们讲代码。以下是服务端(server)与客户端(client)的固定 TCP 编程的步骤,需要熟记!我们在文章末端也放了一套测试代码,仅供参考。

在开始之前,我们还要先了解以下一些socket编程中几个常用的函数和结构体:

1. struct sockaddr_in结构体

struct sockaddr_in {
    sa_family_t sin_family;    // AF_INET(IPv4)
    in_port_t sin_port;        // 端口(必须htons)
    struct in_addr sin_addr;   // IP地址
};

2. 工具函数

  • htons():主机端口 → 网络端口(作用:填写端口号,给sin_port赋值)
  • htonl():主机IP → 网络IP(常用在sin_addr
  • inet_addr():字符串IP → 网络IP(例如inet_addr("192.168.1.100");
  • inet_ntoa():网络IP → IP字符串(打印用inet_ntoa(client_addr.sin_addr)

服务端(server)

1. 定义变量,填写相关信息

    struct sockaddr_in server_addr, client_addr;
    // 清空地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    memset(&client_addr, 0, sizeof(client_addr));

    // 填写服务端地址信息
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

在服务端中,我们需要填写自己的信息,供客户端识别连接

2. socket():创建套接字

    // 创建套接字
    int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
  • AF_INET:ipv4
  • SOCK_STREAM:TCP协议
  • 返回值为文件描述符fd

3. bind():绑定自己的IP和端口

    // 绑定监听套接字
    int ret = bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));

作用:告诉操作系统“我要使用这个 IP 和端口”

4. listen():开启监听

    ret = listen(server_sockfd, 128);

第二个参数表示内核中已完成三次握手、等待被accept的连接队列的最大长度。

5. accept():阻塞等待客户端连接

    // 接受客户端连接
    socklen_t client_addr_len = sizeof(client_addr);
    int client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
    handle_error("accept", client_sockfd);
    printf("客户端 %s 已连接\n", inet_ntoa(client_addr.sin_addr));
  • (struct sockaddr *)&client_addr:用来保存客户端的IP和端口
  • &client_addr_len:客户端地址实际大小
  • 返回值:新文件描述符(用于和客户端收发数据)

6. recv()/send():收发数据

// 从客户端读取消息的线程函数
void *read_from_client(void *arg)
{
    int client_sockfd = *(int *)arg;
    char buffer[1024];
    while (1)
    {
        memset(buffer, 0, sizeof(buffer));
        ssize_t bytes_read = recv(client_sockfd, buffer, sizeof(buffer) - 1, 0);
        if (bytes_read < 0)
        {
            perror("recv");
            break;
        }
        else if (bytes_read == 0)
        {
            printf("--------------------------\n");
            printf("客户端已断开连接\n");
            printf("--------------------------\n");
            break;
        }
        printf("收到客户端消息: %s\n", buffer);
    }
    close(client_sockfd);
    return NULL;
}

// 服务器向客户端发送消息的线程函数
void *write_to_client(void *arg)
{
    int client_sockfd = *(int *)arg;
    char buffer[1024];
    while (1)
    {
        fgets(buffer, sizeof(buffer), stdin);
        buffer[strcspn(buffer, "\n")] = 0; // 去掉换行符
        ssize_t bytes_sent = send(client_sockfd, buffer, strlen(buffer), 0);
        if (bytes_sent < 0)
        {
            perror("send");
            break;
        }
    }
    return NULL;
}

int main(int argc,char *argv[])
{
    pthread_t pid_read, pid_write;
    // 创建线程处理客户端消息
    pthread_create(&pid_read, NULL, read_from_client, (void *)&client_sockfd);
    pthread_create(&pid_write, NULL, write_to_client, (void *)&client_sockfd);

    // 等待线程结束
    pthread_join(pid_read, NULL);
    pthread_join(pid_write, NULL);
}

这里创建了两个线程,一个读线程,一个写线程,服务端既可以读取客户端发来的信息,也可以向客户端发送消息。

7. close():关闭套接字

    close(server_sockfd);
    close(client_sockfd);

最后关闭监听套接字和客户端套接字,我们完成了服务端代码的编写。

客户端(client)

1. 定义变量,填写需要连接的服务端信息

#define SERVERPORT 8080          //服务端的端口
#define SERVERIP "127.0.0.1"     //服务端IP地址

    struct sockaddr_in server_addr;
    // 清空地址结构体
    memset(&server_addr, 0, sizeof(server_addr));

    // 填写连接服务端的地址信息
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVERPORT);
    server_addr.sin_addr.s_addr = inet_addr(SERVERIP);

客户端的端口不需要填写,系统会自动分配其端口

2. socket():创建套接字

    // 创建套接字
    int client_sockfd = socket(AF_INET, SOCK_STREAM, 0);

3. connect():连接服务端

    // 连接服务端
    int ret = connect(client_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    printf("已连接到服务器 %s:%d\n", SERVERIP, SERVERPORT);

参数类别与accept()函数大差不差,唯一区别是这里不用传长度的地址。

4. recv()/send():收发数据

void *read_from_server(void *arg)
void *write_to_server(void *arg)

    pthread_t pid_read, pid_write;
    // 创建线程处理客户端消息
    pthread_create(&pid_read, NULL, read_from_server, (void *)&client_sockfd);
    pthread_create(&pid_write, NULL, write_to_server, (void *)&client_sockfd);

    // 等待线程结束
    pthread_join(pid_read, NULL);
    pthread_join(pid_write, NULL);

两个线程函数与服务端基本是一致的,这里就不写了,改一下参数和表述就可以了。

5. close():关闭套接字

    close(client_sockfd);

最后关闭客户端的套接字,客户端的代码就编写完成了。


运行结果

编写完server.c和client.c后,通过makefile生成可执行文件,打开Linux终端运行两个文件(我们本次使用的是同一个电脑进行网络通信,如果需要实现远程通信,输入ifconfig查看电脑的IP地址,并修改代码中的SERVERIP)。

运行时需要注意的是,先运行server打开服务端,再运行client连接服务端。

如下图所示,双方都可以收发数据。


扩展:多客户端连接

客户端代码不改变,只需修改服务端代码:

    while(1)
    {
        pthread_t pid_read, pid_write;
        socklen_t client_addr_len = sizeof(client_addr);
        int client_sockfd = accept(server_sockfd,(struct sockaddr *)&client_addr,&client_addr_len);
        handle_error("accept",client_sockfd);
        printf("客户端已连接: %s:%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));

        int *pclient_sockfd = malloc(sizeof(int));
        *pclient_sockfd = client_sockfd;
        //创建线程处理客户端连接
        pthread_create(&pid_read,NULL,read_from_client,pclient_sockfd);
        pthread_create(&pid_write,NULL,write_to_client,pclient_sockfd);
        //分离线程,避免僵尸线程
        pthread_detach(pid_read);
        pthread_detach(pid_write);
    } 

加入while(1)循环,把需要用到的参数定义在循环中,这样就可以实现多客户端连接到同一个服务端的功能了。

感谢阅读!如有侵权,请联系作者

感谢支持!
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇