unix网络编程卷一:第四章——基本TCP套接口编程

本文详细介绍了TCP套接字编程的基本流程,包括socket、connect、bind、listen、accept等核心函数的使用方法及注意事项,深入解析了客户端与服务器如何通过套接字建立连接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本TCP套接口编程

4.1 过程

在这里插入图片描述

4.2 socket

#include <sys/socket.h>
int socket(int family, int type, int protocal);

返回值:成功返回非负整数,与文件描述符类似,称为套接口描述字,简称套接字(sockfd)。
注意调用完socket函数之后只是有了一个某一协议下的套接字,并没有指明地址之类的。

  • family:协议族
family说明
AF_INETIPv4
AF_INET6IPv6
AF_LOCALunix协议域

AF_xxx(address family??)地址族。

  • type:套接口类型
type说明
SOCK_STREAM字节流套接口
SOCK_DGRAM数据报套接口
SOCK_RAW原始套接口
  • protocol:协议
protocol说明
IPPROTO_TCPTCP
IPPROTO_UDPUDP
IPPROTO_SCTPSCTP

4.3 connect

从4.1中的图中可以看出connect是客户端调用的一个函数。
客户端在调用connect之前不用调用bind函数,内核会选择IP地址与端口。
connect函数的形式是:

#include <sys/socket.h>
int connect(int sockfd, const sockaddr* servaddr, socklen_t addrlen);
//返回值:0-成功,1-出错

参数解析:

  • sockfd:调用socket()函数返回的套接字;
  • servaddr:指向服务器的套接口地址结构体的指针
  • addrlen:结构体的长度;

如果是TCP套接口,客户端在调用connect函数时,会触发三次握手
而且connect函数只有在成功或出错时才会返回
也就是说,如果connect函数返回了,表明已经成功建立连接或者出错了。

如果连接失败会怎么样呢?会出现什么类型的连接失败呢?

  1. 客户端没有收到服务器对的SYN的响应
    如果超时,客户端会重发SYN,但是多次重发之后无响应则返回ETIMEDOUT错误。

  2. 服务器对客户端发出的SYN的响应为RST
    服务器没有在客户端指定的端口上监听的进程。返回ECONNREFUSED.

  3. 客户端发出的SYN在某个路由器出现目的不可达

如果connect成功了,那么将从SYN_SENT状态转变为established。
如果connect失败了,将直接进入CLOSED状态,则下次不能再使用本次的套接字,必须关闭。

4.4 bind

将IP地址与端口赋予一个套接口。

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen);
//返回值:0-成功, -1-出错

比较关键的是第二个参数,这里有IP地址与端口号。

bind可以绑定IP地址与端口号,也可以不绑定。
对于客户端,如果不绑定IP地址,则内核在connect的时候会自动选择;
对于服务器,内核把客户端发送的SYN中的目的IP地址当做服务器的源IP地址。

如果指定的端口号为0,则内核在bind时会选择一个端口;
如果指定的IP地址为通配地址,那么内核在已连接(TCP)或者发送数据报(UDP)时才选择IP地址。
关于通配地址:IPv4的通配地址为INADDR_ANY,一般为0。

4.5 listen

只用于服务端。
形式:

#include <sys/socket.h>
int listen(int sockfd, int backlog);

作用:
1)使用socket函数创建套接字时,默认创建的是主动套接字,也就是说他会主动发起connect,但是对于服务器端,应该是被动地被连接,所以listen函数就把主动套接字变为被动套接字,使得服务端从CLOSED状态转变为LISTEN状态。
2)规定内核应该为相应套接口排队的最大连接数。

这里说的最大连接数是什么意思呢?
实际上,内核为监听套接口维护两个队列,分别是:

  1. 未完成连接队列:SYN已经由客户端发出并到达服务器,但是服务器正在等待进行TCP三次握手的过程,此时套接口处于SYN_RCVD状态。
  2. 已完成连接队列:每个已完成TCP三次握手的客户对应一项。套接口处于established状态。

在这里插入图片描述
从上图可以看出,每到达一个SYN,TCP在未完成队列中创建一个新项,三次握手完成之后,该项就从未完成队列转移到已完成连接队列的尾部,该项将一直存在直到三次握手完成或者超时。当进程调用accept时,从已完成队列中取出队头。

在这里插入图片描述

4.6 accept

服务器调用,用处:从已完成连接队列队头返回下一个已完成连接。如果队列为空,则进入睡眠(假设套接口为阻塞方式)。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen);

参数struct sockaddr* cliaddr用来返回客户端地址信息。
这个函数会返回一个套接字,注意返回的套接字为已连接套接字,和第一个参数“监听套接字”不同。
一个服务器通常只有一个监听套接字,但是每建立一个连接就会产生一个新的已连接套接字。监听套接字的生命周期与服务器进程生命周期相同,但是已连接套接字在连接释放之后就被关闭了。

4.8 fork与并发服务器

为每一个新到来的连接创建一个进程,在父进程中继续监听,在子进程中完成对客户需求的处理。

连接建立之后,accept返回一个已连接套接字connfd,此时调用fork,在父进程中直接关闭connfd,在子进程中关闭listenfd,执行操作然后退出

框架:

pid_t pid;
int listenfd, connfd;
listenfd = socket(...);
bind(listenfd, ...);
listen(listenfd, ...);
for(;;) {
	connfd = accept(listenfd, ...);
	if( (pid = fork()) == 0 ) { // child proc
		close(listenfd); //子进程关闭listefd
		do_something();	 //子进程处理函数
		close(connfd);	 //关闭connfd
		exit(0);		 //子进程退出
	}
	// father proc
	close(connfd);//父进程直接关闭connfd, 然后等待下一个连接
}

调用close会导致发送一个FIN,但是上面的例子中父进程close(connfd)但是没有断开与客户端的连接,这是为什么呢?

其实是因为执行fork函数时,套接字connfd的引用计数加一了,在父进程中执行close并没有将connfd的引用计数变为0,所以连接没断。
注意:fork之后的文件描述符以及套接字由父子进程共享,因此引用计数会加一。

在这里插入图片描述
子进程关闭listenfd,父进程关闭connfd的过程:
在这里插入图片描述

4.9 close

#include <unistd.h>
int close(int sockfd);

默认行为:将套接口标记为已关闭,立即返回,被标记为已关闭的套接口不能再被write与read函数调用。
但是这种默认行为可以通过某种方式改变,如套接口选项SO_LINGER。

在上一节并发服务器中说了文件描述符与引用计数的问题,父进程中关闭只会使得相应的套接字引用计数减一,只有在引用计数变为0之后才会触发FIN的发送。

如果就是想发送一个FIN,则应该调用shutdown函数。

父进程一定记得关闭connfd,否则会耗尽套接字,而且连接不会被终止。
在这里插入图片描述

在这里插入图片描述

getsockname()返回本地地址;
getpeername()返回客户端地址;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值