TCP 5(socket编程)

TCP的socket编程

TCP Socket 编程是网络通信的核心技术,它让不同计算机上的程序能够通过网络进行可靠的、面向连接的数据交换。

Socket 是什么?

Socket(套接字)是操作系统提供的一套网络编程接口(API),它封装了底层复杂的TCP/IP协议细节。一个Socket由 IP地址端口号 唯一确定,就像一部电话需要"电话号码(IP地址)"和"分机号(端口号)"才能接通。

TCP Socket 通信模型

TCP通信遵循经典的客户端-服务器(C/S)模型。整个过程就像打电话:服务器先"开机"等待,客户端再"拨号"连接,接通后双方才能"通话"。

服务器端流程 (被动方)

服务器端负责监听并响应客户端的连接请求,其标准步骤如下:

  1. socket(): 创建一个套接字。这相当于准备一部电话机。
  2. bind(): 将套接字绑定到指定的IP地址和端口号上。这相当于给电话机分配一个固定的号码,以便客户端能找到它。
  3. listen(): 将套接字设置为监听模式,开始等待连接请求。这相当于让电话机进入待机状态,等待来电。
  4. accept(): 阻塞等待,当有客户端连接时,接受该连接。这个函数会返回一个全新的套接字,专门用于和这个客户端通信。原来的监听套接字则继续等待其他客户端的连接。
  5. send() / recv(): 使用新的套接字与客户端进行数据的发送和接收。
  6. 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);
	}
}
客户端流程 (主动方)

客户端主动向服务器发起连接,流程相对简单:

  1. socket(): 创建一个套接字。
  2. connect(): 向服务器的IP地址和端口号发起连接请求。这一步会触发TCP的"三次握手"过程,与服务器建立连接。
  3. send() / recv(): 连接成功后,即可与服务器进行数据交换。
  4. 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 参数的工作机制

  1. 连接排队:服务器调用 listen 后,会创建一个"已完成连接队列"。当客户端连接完成三次握手,连接就进入这个队列,等待服务器调用 accept 函数将其取出并进行后续的业务处理。
  2. 队列已满:如果新的连接请求不断到来,而服务器处理连接的速度(accept 的速度)跟不上,队列就会逐渐被填满。一旦队列中的连接数达到 backlog 设定的值,内核就会开始拒绝新的连接请求。
  3. 拒绝表现:对于被拒绝的连接,客户端通常会收到一个 "Connection refused" 错误,或者因为连接请求(SYN包)被忽略而最终导致连接超时。

accept 发生在三次握手的哪一步

accept 函数并不直接参与 TCP 三次握手的任何一个步骤,它是在三次握手完全成功之后,由服务器端应用程序调用的。

简单来说,三次握手是操作系统内核自动完成的,而 accept 是应用层程序从内核那里"领取"已经建立好的连接。

核心机制:两个连接队列

为了理解 accept 的作用,我们需要了解内核为每个监听套接字维护的两个队列:

  1. 半连接队列 (SYN Queue)

    • 用于存放尚未完成三次握手的连接。
    • 当服务器收到客户端的第一个 SYN 包后,会将这个连接请求放入半连接队列,并回复 SYN+ACK。此时连接处于 SYN_RECV 状态。
  2. 全连接队列 (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(),会产生以下后果:

  1. 连接堆积 :已建立的连接会一直停留在内核的全连接队列中,等待应用程序来取。
  2. 队列满后拒绝服务 :全连接队列的大小是有限的(受限于 backlog 参数和系统配置 net.core.somaxconn)。一旦队列被填满,内核就会丢弃新的连接请求(或者发送 RST 包重置连接),导致新的客户端无法连接。
  3. 数据缓冲 :有趣的是,在 accept() 被调用之前,如果客户端发送了数据,内核通常会将这些数据缓冲在接收缓冲区中。一旦应用程序随后调用了 accept(),依然可以读到之前发送的数据。

accept() 只是为了让应用程序"认领"连接,连接本身的建立是内核在后台默默完成的。

相关推荐
风曦Kisaki2 小时前
Linux服务Day03:自定义YUM仓库、网络YUM仓库(HTTP/FTP)、MariaDB数据库基础操作
linux·网络·数据库
xcbeyond2 小时前
Linux 磁盘挂载
linux·运维·服务器
智擎软件测评小祺3 小时前
渗透测试报告撰写:漏洞发现到验证流程
网络·渗透测试·测试·cma·第三方检测·cnas·渗透测试报告
攻城狮在此3 小时前
华三交换机端口隔离配置(VLAN内二层互访隔离)
网络·安全
z10_143 小时前
享住宅IP、长效代理ip是什么?有什么用?
网络·网络协议·tcp/ip
小羽网安3 小时前
Linux 服务器如何进行安全加固?
linux·服务器·安全
振浩微433射频芯片3 小时前
433MHz在智能家居中的应用大全(一):智能窗帘篇——为什么稳定比花哨更重要?
网络·单片机·嵌入式硬件·物联网·智能家居
上海云盾安全满满3 小时前
服务器如果做好日常维护,有什么作用
运维·服务器
翻斗包菜3 小时前
Python 网络编程从入门到精通:TCP/UDP/Socket 实战详解
网络·python·tcp/ip