C Tcp通信

学完了c程序基础知识后,是不是感觉什么也干不了,总想找点事情来练手?本文暂且介绍一下,如何使用socket进行tcp通信;示例程序目前并不健壮,仅为演示socket通信的基本流程。
image

用白话理解tcp通信

TCP通信就像打电话,日常生活中我们打电话,需要进行下面一些动作:
1、拿起电话
2、输入对方号码发起通话
3、进行交谈
4、通话完毕挂断电话
打电话这个过程,在tcp通信协议中有两个比较重要的知识点要介绍:

三次握手

如上面打电话的第二步,发起通话,在tcp协议中这一步称之为建立连接,由于建立连接需要三次消息发送,故将建立连接这个阶段称为“三次握手”
用形象的文字描述三次握手:

  • 你好,听的到么
  • 听得到,你听得到我说话么

这三步对应到tcp中分别是:

  • 通信方A 发送syn标志 A处于syn_send状态
  • 通信方B 回复一个 syn + ack 表示确认收到对方发过来的syn信号 B处于syn-recv状态
  • 通信方A收到syn+ack后处于 established状态(已建立连接状态) 再次发送一个ack信号 表示我也接收到对方回复的ack信号 若B收到ack后,B也会处于established状态(已建立连接状态)

经历上述三步后就代表连接已成功建立。。。。

题外话

  • 为什么是三次握手?第一步自不必说,若没有第二步,B有没有听到我讲话都无法确认,如何再继续交流?若没有第三步,B不知道A有没有听到B的声音,他也不知道如何继续交流。。
  • tcp dos攻击:搞一堆不存在的ip向服务端发起连接,服务端回复syn ack后,将生出一堆 syn_recv状态的连接,由于这些ip不存在,所以服务端收不到 ack信号。。。半连接队列满后,服务端无法再接收更多的连接,服务器瘫痪。。

四次挥手

在上面打电话的例子中的第四步:挂断电话,这一步在tcp协议中,也有对应一个很重要的过程“四次挥手”--断开连接;通信双方断开连接,就像打电话,需要再三谨慎确认对方是不是没有可交代的事情了
用形象的文字描述四次挥手:

  • 那没啥事儿挂了吧
  • 我这没啥事儿了 你挂吧

这四步对应到tcp中分别是:

  • 通信方A发起断开请求 fin+ack A进入终止等待1 fin_wait_1
  • 通信方B回复ack 确认收到 B进入关闭等待状态 close_wait A进入终止等待2 fin_wait_2
  • 通信方B发送释放信号 fin+ack 进入最后确认状态last_ack状态 A进入时间等待状态 time_wait

时间等待状态很重要 为什么要有这个等待状态?因为要保证B一定能收到自己发出的fin中的ack;这个期间如果通信方B收不到ack信号,会再次重发fin,处在time_wait的A可以再次发送ack。

  • 通信方A回复ack 确认释放 双方关闭连接

好了三次握手、四次挥手介绍完毕,下面开始介绍 socket对象;在c程序中通过socket对象进行tcp通信。

客户端代码实现

对于客户端需要以下几步:

  • 创建套接字对象 指定ipv4/ipv6 指定 tcp/udp
  • 绑定本地地址和端口
  • 连接目标主机(需要指定目标主机的地址和端口)
  • 接收或发送消息
  • 关闭套接字对象
#include <stdio.h>//控制台输入输出函数所在头文件
#include <string.h>//清空字符串函数所在头文件
#include <sys/socket.h>//套接字结构体所在头文件
#include <arpa/inet.h>//字节序网络序所在头文件
#include <unistd.h>//close套接字所在函数
#include <signal.h>//接收程序退出信号的函数所在头文件
#include <stdlib.h>//exit函数所在头文件

int socketfd;

void handle_exit() {
    if (socketfd >= 0) {
        close(socketfd);
        puts("释放套接字");
    }
}


void handle_signal(int sig) {
    puts("收到程序退出信息,正在处理...");
    exit(EXIT_SUCCESS);
}

int main(void) {
    signal(SIGINT, handle_signal);
    signal(SIGTERM, handle_signal);
    atexit(handle_exit);
    //创建套接字 返回文件描述符 非负值标识创建成功
    socketfd = socket(AF_INET, SOCK_STREAM, 0); //地址族 套接字类型 套接字协议

    //绑定地址 传入套接字描述符 传入网络信息
    //创建目标地址信息
    struct sockaddr_in local_addr;
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = htons(2000);
    local_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bind(socketfd, (struct sockaddr *) &local_addr, sizeof(local_addr));
    //发起连接
    struct sockaddr_in remote_addr;
    remote_addr.sin_family = AF_INET;
    remote_addr.sin_port = htons(8001);
    remote_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    printf("向%s:%d发起连接...\n", inet_ntoa(remote_addr.sin_addr),ntohs(remote_addr.sin_port));
    connect(socketfd, (struct sockaddr *) &remote_addr, sizeof(remote_addr));
    puts("连接成功...");
    char buf[20];
    while (1) {
        memset(buf, 0, sizeof(buf));
        puts("输入要消息后按回车发送(直接按回车后退出程序)");
        fgets(buf, sizeof(buf) - 1, stdin);
        size_t len = strlen(buf);
        if (len > 0 && buf[len - 1] == '\n') {
            buf[len - 1] = '\0';
        }
        if (strlen(buf) == 0) {
            puts("退出发送");
            break;
        }
        printf("发送数据:%s,发送长度:%lu\n", buf, strlen(buf));
        send(socketfd, buf, strlen(buf), 0);
    }
    close(socketfd);
    return 0;
}

上面代码的介绍:

  • 主机字节序和网络字节序,网络字节序都是大端存储,主机字节序不确定是大端还是小端;所以对于ip地址(字符串),需要转换成网络字节序;对于端口(整型),也需要转换成网络字节序;整型转网络字节序、字符串转网络字节序函数不同
  • 注册退出信号回调函数,是为了在退出程序时能正确释放资源

服务端代码实现

服务端的实现需要下面几步:

  • 创建socket对象
  • 绑定本地地址和端口
  • 开启连接监听
  • 调用接收连接函数准备接收来自客户端的连接
  • 接收到来自客户端的连接后 根据获得的客户端的socket指针 与 客户端进行消息发送和接收
  • 通信完毕 释放客户端socket对象 释放服务端socket对象
#include <stdio.h>//控制台输入输出函数所在头文件
#include <string.h>//清空字符串函数所在头文件
#include <sys/socket.h>//套接字结构体所在头文件
#include <arpa/inet.h>//字节序网络序所在头文件
#include <unistd.h>//close套接字所在函数
#include <signal.h>//接收程序退出信号的函数所在头文件
#include <stdlib.h>//exit函数所在头文件

int socket_fd;

void handle_signal(int sig) {
    puts("用户退出程序");
    if (socket_fd > 0) {
        puts("停止服务");
        close(socket_fd);
    }
    exit(0);
}

int main(void) {
    signal(SIGINT, handle_signal);//解决程序被用户手动关闭 无法正常停止服务的问题
    signal(SIGTERM, handle_signal);
    //创建套接字 指定ipv4 套接字类型 套接字协议默认
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    //在bind之前设置socket 允许重用本地端口
    int opt = 1;
    int set_ret = setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR, &opt, sizeof(opt));
    if (set_ret != 0) {
        perror("setsockopt error");
        close(socket_fd);
        exit(-1);
    }

    //绑定本地网络信息到套接字
    struct sockaddr_in local_addr;
    local_addr.sin_family = AF_INET;
    local_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    local_addr.sin_port = htons(8001);
    int ret = bind(socket_fd, (struct sockaddr *) &local_addr, sizeof(local_addr));
    if (ret != 0) {
        perror("bind error");
        close(socket_fd);
        exit(-1);
    }
    //监听连接
    ret = listen(socket_fd, 5);
    if (ret != 0) {
        perror("listen error");
        close(socket_fd);
        exit(-1);
    }
    printf("服务正在监听%s:%d...\n", inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
    //接受连接
    int client_fd = accept(socket_fd, NULL, NULL);
    printf("收到了一个客户端连接\n");
    //接收或发送消息
    char buf[200];
    while (1) {
        memset(buf, 0, sizeof(buf));
        ssize_t recv_len = recv(client_fd, buf, sizeof(buf) - 1, 0); //最后一个参数0 代表阻塞
        if (recv_len == 0) {
            //客户端主动关闭连接
            puts("client closed");
            break;
        }
        if (recv_len <= 0) {
            perror("recv error");
            break;
        }
        printf("收到消息: %s\n", buf);
    }
    close(client_fd);
    close(socket_fd);
    return 0;
}

上面代码的介绍:

  • 我们知道四次挥手有时间等待状态的存在,它的存在能大概“保活”端口30~120秒左右;而程序异常退出等无法正常close套接字对象的场景,会让操作系统将连接置于time_wait状态,故上面代码增加了对套接字的“端口地址重用”设置;这样便降低了意外关闭的服务端再次启动后出现“Address already in use”错误的概率。
  • recv函数收到数据长度小于0 代码出现错误;等于0代表客户端断开连接;程序中要正确处理这两种情况
  • 释放操作不能忘

示例代码还有许多可以完善的地方,但用来瞥见tcp通信应该是够了。

posted @ 2025-11-22 22:33  BigBosscyb  阅读(5)  评论(0)    收藏  举报