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

用白话理解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通信应该是够了。

浙公网安备 33010602011771号