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

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中分别是:
  • 通信方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), stdin);if (buf[0] == 0 || buf[0] == '\n') {close(socketfd);break;}send(socketfd, buf, sizeof(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是全双工通信,上面代码没有演示,但这并不影响 客户端和服务端各自即可以发消息也可以收消息

以上便是对c程序中通过socket对象进行tcp通信的简要介绍。

http://www.jsqmd.com/news/47912/

相关文章:

  • SZMS 251019 订题赛笔记
  • 关于面向对象程序设计的第一阶段大作业总结
  • Spring Boot核心知识点全解析 - 实践
  • RHCA - DO374 | Day03:通过自动化控制器运行剧本 - 详解
  • 离职/毕业-清理电脑
  • 2025.11.22
  • 在 Python 和 NumPy 的常规书写规范中,ndarray需要大写吗?
  • ddddocr: 对图片处理提升识别率
  • `np.array` 和 `np.ndarray`是什么关系?
  • 大数据部门和AI部门边界
  • Post Processing
  • 工作草稿
  • 【Rust编程:从新手到大师】Rust 环境搭建(详细版) - 教程
  • 2025年11月南通宠物医疗市场深度分析:专业服务与行业规范的标杆选择
  • 软工团队作业3
  • 电梯调度迭代编程作业复盘
  • 球坐标系的大运动方程组
  • 【数据库】navicat的下载以及数据库约束 - 实践
  • i2c linux
  • hyper-v linux
  • 二维费用背包 尽量前后一直把好像也没关系
  • 详细介绍:在Windows中的Docker与WSL2的关系,以及与WSL2中安装的Ubuntu等其它实例的关系
  • ThinkPHP5 RCE+Linux find提权渗透实战:原理+复现(CVE-2018-20062) - 详解
  • 团队作业3--需求改进与系统设计
  • 单部电梯调度总结
  • hyper for linux
  • https linux
  • 页面
  • Trick——数据结构
  • 锂矿及其投资机会