TCP的socket编程
TCP Socket 编程是网络通信的核心技术,它让不同计算机上的程序能够通过网络进行可靠的、面向连接的数据交换。
Socket 是什么?
Socket(套接字)是操作系统提供的一套网络编程接口(API),它封装了底层复杂的TCP/IP协议细节。一个Socket由 IP地址 和 端口号 唯一确定,就像一部电话需要"电话号码(IP地址)"和"分机号(端口号)"才能接通。
TCP Socket 通信模型
TCP通信遵循经典的客户端-服务器(C/S)模型。整个过程就像打电话:服务器先"开机"等待,客户端再"拨号"连接,接通后双方才能"通话"。
服务器端流程 (被动方)
服务器端负责监听并响应客户端的连接请求,其标准步骤如下:
socket(): 创建一个套接字。这相当于准备一部电话机。bind(): 将套接字绑定到指定的IP地址和端口号上。这相当于给电话机分配一个固定的号码,以便客户端能找到它。listen(): 将套接字设置为监听模式,开始等待连接请求。这相当于让电话机进入待机状态,等待来电。accept(): 阻塞等待,当有客户端连接时,接受该连接。这个函数会返回一个全新的套接字,专门用于和这个客户端通信。原来的监听套接字则继续等待其他客户端的连接。send()/recv(): 使用新的套接字与客户端进行数据的发送和接收。close(): 通信结束后,关闭连接,释放资源。
代码示例:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
//创建套接字(打开文件,文件描述符)套接字可以通过网络收发数据
int sockfd =socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
exit(1);
}
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(8090);//临时端口
saddr.sin_addr.s_addr=inet_addr("192.168.101.134");
int res =bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定,制定应用程序使用的ip端口
if(res==-1)
{
printf("bind err\n");
}
res=listen(sockfd,5);
if(res==-1)
{
exit(1);
}
while(1)
{
int len = sizeof(caddr);//accept 接收连接,没人连接,则阻塞
int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c<0)
{
break;
}
printf("accept c=%d\n",c);
char buff[128]={0};
int n=recv(c,buff,127,0);//接收连接,会阻塞
printf("recv:&s\n",buff);
send(c,"ok",2,0);//发送数据
close(c);
}
}
客户端流程 (主动方)
客户端主动向服务器发起连接,流程相对简单:
socket(): 创建一个套接字。connect(): 向服务器的IP地址和端口号发起连接请求。这一步会触发TCP的"三次握手"过程,与服务器建立连接。send()/recv(): 连接成功后,即可与服务器进行数据交换。close(): 通信结束后,关闭连接shi
代码示例:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
//创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1)
{
exit( 1 );
}
struct sockaddr_in saddr;//指定服务器的ip和端口
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8090);//htons转为网络字节序列
saddr.sin_addr.s_addr = inet_addr("192.168.1.101");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1)
{
printf("connect err\n");
exit(1);
}
char buff[128] = {0};
printf("intput:\n");
fgets(buff,128,stdin);
send(sockfd,buff,strlen(buff)-1,0);//write
memset(buff,0,sizeof(buff));
recv(sockfd,buff,127,0);
printf("buff=%s\n",buff);
close(sockfd);
exit(0);
}
TCP Socket编程中几个最核心的函数
| 函数 | 作用 | 调用方 |
|---|---|---|
socket() |
创建套接字,返回一个文件描述符 | 客户端 & 服务器 |
bind() |
将套接字与IP和端口绑定 | 通常是服务器 |
listen() |
开启监听,准备接受连接 | 服务器 |
accept() |
接受一个客户端连接,返回新套接字 | 服务器 |
connect() |
主动与服务器建立连接 | 客户端 |
send() / recv() |
发送和接收数据 | 客户端 & 服务器 |
close() |
关闭套接字 | 客户端 & 服务器 |
listen函数中的backlog参数:用于指定等待被应用程序处理的已完成连接队列的最大长度。简单来说,当客户端发起 TCP 连接请求并成功完成三次握手后,这个已建立的连接并不会立刻被应用程序处理,而是先进入内核维护的一个队列中排队。
backlog参数就是用来设定这个队列的容量上限。
listen函数中的backlog参数的工作机制
- 连接排队:服务器调用
listen后,会创建一个"已完成连接队列"。当客户端连接完成三次握手,连接就进入这个队列,等待服务器调用accept函数将其取出并进行后续的业务处理。- 队列已满:如果新的连接请求不断到来,而服务器处理连接的速度(
accept的速度)跟不上,队列就会逐渐被填满。一旦队列中的连接数达到backlog设定的值,内核就会开始拒绝新的连接请求。- 拒绝表现:对于被拒绝的连接,客户端通常会收到一个 "Connection refused" 错误,或者因为连接请求(SYN包)被忽略而最终导致连接超时。
accept 发生在三次握手的哪一步
accept函数并不直接参与 TCP 三次握手的任何一个步骤,它是在三次握手完全成功之后,由服务器端应用程序调用的。简单来说,三次握手是操作系统内核自动完成的,而
accept是应用层程序从内核那里"领取"已经建立好的连接。核心机制:两个连接队列
为了理解
accept的作用,我们需要了解内核为每个监听套接字维护的两个队列:
半连接队列 (SYN Queue)
- 用于存放尚未完成三次握手的连接。
- 当服务器收到客户端的第一个
SYN包后,会将这个连接请求放入半连接队列,并回复SYN+ACK。此时连接处于SYN_RECV状态。全连接队列 (Accept Queue)
- 用于存放已经完成三次握手的连接。
- 当服务器收到客户端的最后一个
ACK包,标志着三次握手成功。此时,内核会将该连接从"半连接队列"移动到"全连接队列",连接状态变为ESTABLISHED。
accept的工作时机
accept函数的作用就是从全连接队列中取出一个已经建立好的连接。
- 如果全连接队列不为空,
accept会立即取出队首的连接,并返回一个新的套接字(socket)文件描述符,应用程序后续就使用这个新的套接字与客户端进行通信。- 如果全连接队列为空,
accept调用会阻塞,直到有新的连接完成三次握手并被放入队列中。因此,
accept发生在三次握手流程之外,是应用程序处理已建立连接的步骤。即使服务器端不调用
accept()方法,TCP 连接依然可以建立,三次握手也会照常完成。这是因为
accept()并不参与 TCP 的三次握手过程。
accept() 在流程中的位置:
| 步骤 | 动作 | 执行者 | 状态/结果 |
|---|---|---|---|
| 1 | 客户端发送 SYN | 客户端内核 | 请求连接 |
| 2 | 服务端回复 SYN+ACK | 服务端内核 | 同意连接,放入半连接队列 |
| 3 | 客户端发送 ACK | 客户端内核 | 三次握手完成 |
| 4 | 连接移入全连接队列 | 服务端内核 | 连接已建立 (ESTABLISHED) |
| 5 | 调用 accept() | 应用程序 | 从队列取出连接 (此时才发生) |
虽然连接能建立,但如果不调用 accept(),会产生以下后果:
- 连接堆积 :已建立的连接会一直停留在内核的全连接队列中,等待应用程序来取。
- 队列满后拒绝服务 :全连接队列的大小是有限的(受限于
backlog参数和系统配置net.core.somaxconn)。一旦队列被填满,内核就会丢弃新的连接请求(或者发送 RST 包重置连接),导致新的客户端无法连接。 - 数据缓冲 :有趣的是,在
accept()被调用之前,如果客户端发送了数据,内核通常会将这些数据缓冲在接收缓冲区中。一旦应用程序随后调用了accept(),依然可以读到之前发送的数据。
accept() 只是为了让应用程序"认领"连接,连接本身的建立是内核在后台默默完成的。