你好,我是LMOS。
上节课,我们一起了解了套接字的工作机制和数据结构,但套接字有哪些基本接口实现呢?相信学完这节课,你就能够解决这个问题了。
今天我会和你探讨套接字从创建、协议接口注册与初始化过程,还会为你深入分析套接字系统,是怎样调用各个功能函数的。通过这节课,相信你可以学会基于套接字来编写网络应用程序。有了之前的基础,想理解这节课并不难,让我们正式开始吧。
套接字接口
套接字接口最初是BSD操作系统的一部分,在应用层与TCP/IP协议栈之间接供了一套标准的独立于协议的接口。
Linux内核实现的套接字接口,将UNIX的“一切都是文件操作”的概念应用在了网络连接访问上,让应用程序可以用常规文件操作API访问网络连接。
从TCP/IP协议栈的角度来看,传输层以上的都是应用程序的一部分,Linux与传统的UNIX类似,TCP/IP协议栈驻留在内核中,与内核的其他组件共享内存。传输层以上执行的网络功能,都是在用户地址空间完成的。
Linux使用内核套接字概念与用户空间套接字通信,这样可以让实现和操作变得更简单。Linux提供了一套API和套接字数据结构,这些服务向下与内核接口对接,向上与用户空间接口对接,应用程序正是使用这一套API访问内核中的网络功能。
套接字的创建
在应用程序使用TCP/IP协议栈的功能之前,我们必须调用套接字库函数API创建一个新的套接字,创建好以后,对库函数创建套接字的调用,就会转换为内核套接字创建函数的系统调用。
这时,完成的是通用套接字创建的初始化功能,跟具体的协议族并不相关。
这个过程具体是这样的,在应用程序中执行socket函数,socket产生系统调用中断执行内核的套接字分路函数sys_socketcall,在sys_socketcall套接字函数分路器中将调用传送到sys_socket函数,由sys_socket函数调用套接字的通用创建函数sock_create。
sock_create函数完成通用套接字创建、初始化任务后,再调用特定协议族的套接字创建函数。
这样描述你可能还没有直观感受,我特意画了图,帮你梳理socket创建的流程,你可以对照图片仔细体会调用过程。

结合图解,我再用一个具体例子帮你加深理解,比如由AF_INET协议族的inet_create函数完成套接字与特定协议族的关联。
一个新的struct socket数据结构起始由sock_create函数创建,该函数直接调用__sock_create函数,__sock_create函数的任务是为套接字预留需要的内存空间,由sock_alloc函数完成这项功能。
这个sock_alloc函数不仅会为struct socket数据结构实例预留空间,也会为struct inode数据结构实例分配需要的内存空间,这样可以使两个数据结构的实例相关联。__sock_create函数代码如下。
1 | static int __sock_create(struct net *net, int family, int type, int protocol, |
sock_alloc函数如下所示。
1 | static struct socket *sock_alloc(void) { |
当具体的协议与新套接字相连时,其内部状态的管理由协议自身维护。
现在,函数将struct socket数据结构的struct proto_ops *ops设置为NULL。随后,当某个协议族中的协议成员的套接字创建函数被调用时,ops将指向协议实例的操作函数。这时将struct socket数据结构的flags数据域设置为0,创建时还没有任何标志需要设置。
在之后的调用中,应用程序调用send或receive套接字库函数时会设置flags数据域。最后将其他两个数据域sk和file初始化为NULL。sk数据域随后会把由协议特有的套接字创建函数设置为指向内部套接字结构。file将在调用sock_ma_fd函数时设置为分配的文件返回的指针。
文件指针用于访问打开套接字的虚拟文件系统的文件状态。在sock_alloc函数返回后,sock_create函数调用协议族的套接字创建函数err =pf->create(net, sock, protocol),它通过访问net_families数组获取协议族的创建函数,对于TCP/IP协议栈,协议族将设置为AF_INET。
套接字的绑定
创建完套接字后,应用程序需要调用sys_bind函数把套接字和地址绑定起来,代码如下所示。
1 | asmlinkage long sysbind (bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen) |
结合代码,我们可以看到,sys_bind函数首先会查找套接字对应的socket实例,调用sockfd_lookup_light。在绑定之前,将用户空间的地址拷贝到内核空间的缓冲区中,在拷贝过程中会检查用户传入的地址是否正确。
等上述的准备工作完成后,就会调用inet_bind函数来完成绑定操作。inet_bind函数代码如下所示。
1 | int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len) |
主动连接
因为应用程序处理的是面向连接的网络服务(SOCK_STREAM或SOCK_SEQPACKET),所以在交换数据之前,需要在请求连接服务的进程(客户)与提供服务的进程(服务器)之间建立连接。
当应用程序调用connect函数发出连接请求时,内核会启动函数sys_connect,详细代码如下。
1 | int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen) |
连接成功会返回socket的描述符,否则会返回一个错误码。
监听套接字
调用listen函数时,应用程序触发内核的sys_listen函数,把套接字描述符fd对应的套接字设置为监听模式,观察连接请求。详细代码你可以看看后面的内容。
1 | int __sys_listen(int fd, int backlog) |
被动接收连接
前面说过主动连接,我们再来看看被动接受连接的情况。接受一个客户端的连接请求会调用accept函数,应用程序触发内核函数sys_accept,等待接收连接请求。如果允许连接,则重新创建一个代表该连接的套接字,并返回其套接字描述符,代码如下。
1 | int __sys_accept4_file(struct file *file, unsigned file_flags, |
这个新的套接字描述符与最初创建套接字时,设置的套接字地址族与套接字类型、使用的协议一样。原来创建的套接字不与连接关联,它继续在原套接字上侦听,以便接收其他连接请求。
发送数据
套接字应用中最简单的传送函数是send,send函数的作用类似于write,但send函数允许应用程序指定标志,规定如何对待传送数据。调用send函数时,会触发内核的sys_send函数,把发送缓冲区的数据发送出去。
sys_send函数具体调用流程如下。
1.应用程序的数据被复制到内核后,sys_send函数调用sock_sendmsg,依据协议族类型来执行发送操作。
2.如果是INET协议族套接字,sock_sendmsg将调用inet_sendmsg函数。
3.如果采用TCP协议,inet_sendmsg函数将调用tcp_sendmsg,并按照TCP协议规则来发送数据包。
send函数返回发送成功,并不意味着在连接的另一端的进程可以收到数据,这里只能保证发送send函数执行成功,发送给网络设备驱动程序的数据没有出错。
接收数据
recv函数与文件读read函数类似,recv函数中可以指定标志来控制如何接收数据,调用recv函数时,应用程序会触发内核的sys_recv函数,把网络中的数据递交到应用程序。当然,read、recvfrom函数也会触发sys_recv函数。具体流程如下。
1.为把内核的网络数据转入应用程序的接收缓冲区,sys_recv函数依次调用sys_recvfrom、sock_recvfrom和__sock_recvmsg,并依据协议族类型来执行具体的接收操作。
2.如果是INET协议族套接字,__sock_recvmsg将调用sock_common_recvmsg函数。
3.如果采用TCP协议,sock_common_recvmsg函数将调用tcp_recvmsg,按照TCP协议规则来接收数据包
如果接收方想获取数据包发送端的标识符,应用程序可以调用sys_recvfrom函数来获取数据包发送方的源地址,下面是sys_recvfrom函数的实现。
1 | int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags, |
关闭连接
最后,我们来看看如何关闭连接。当应用程序调用shutdown函数关闭连接时,内核会启动函数sys_shutdown,代码如下。
1 | int __sys_shutdown(int fd, int how) |
重点回顾
好,这节课的内容告一段落了,我来给你做个总结。这节课我们继续研究了套接字在Linux内核中的实现。
套接字是UNIX兼容系统的一大特色,Linux在此基础上实现了内核套接字与应用程序套接字接口,在用户地址空间与内核地址空间之间提供了一套标准接口,实现应用套接字库函数与内核功能之间的一一对应,简化了用户地址空间与内核地址空间交换数据的过程。
通过应用套接字API编写网络应用程序,我们可以利用Linux内核TCP/IP协议栈提供的网络通信服务,在网络上实现应用数据快速、有效的传送。除此之外,套接字编程还可以使我们获取网络、主机的各种管理、统计信息。
创建套接字应用程序一般要经过后面这6个步骤。
1.创建套接字。
2.将套接字与地址绑定,设置套接字选项。
3.建立套接字之间的连接。
4.监听套接字
5.接收、发送数据。
6.关闭、释放套接字。
思考题
我们了解的TCP三次握手,发生在socket的哪几个函数中呢?
欢迎你在留言区跟我交流,也推荐你把这节课转发给有需要的朋友。
我是LMOS,我们下节课见!
- neohope 👍(5) 💬(1)
四次挥手过程分析下【V5.8,正常流程】 5、客户端收到FIN包,子状态从TCP_FIN_WAIT2变为TCP_TIME_WAIT,返回ACK包 A、状态和子状态都为TCP_TIME_WAIT 【tcp_protocol.handler】tcp_v4_rcv-> ->if (sk->sk_state == TCP_TIME_WAIT) goto do_time_wait; ->do_time_wait: ->tcp_timewait_state_process ->->if (tw->tw_substate == TCP_FIN_WAIT2) ->->tw->tw_substate = TCP_TIME_WAIT; ->->inet_twsk_reschedule,重新设置回调时间 ->->return TCP_TW_ACK;
B、返回ACK
->case TCP_TW_ACK:
->tcp_v4_timewait_ack(sk, skb);6、服务端收到ACK包,状态从TCP_LAST_ACK变为TCP_CLOSE
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process
->case TCP_LAST_ACK:
->tcp_done
->->tcp_set_state(sk, TCP_CLOSE);7、客户端超时回调
A、超时时间定义
#define TCP_TIMEWAIT_LEN (60*HZ)
#define TCP_FIN_TIMEOUT TCP_TIMEWAIT_LENB、超时后,回调tw_timer_handler->inet_twsk_kill,进行inet_timewait_sock清理工作
C、没有找到状态变从TCP_TIME_WAIT变为TCP_CLOSE的代码
D、只看没调,有问题的,欢迎小伙伴告诉一下
2021-08-15 - neohope 👍(3) 💬(0)
四次挥手过程分析上【V5.8,正常流程】
1、客户端主动断开连接,状态从TCP_ESTABLISHED变为TCP_FIN_WAIT1,发送FIN包给服务端
A、状态变为TCP_FIN_WAIT1
tcp_close->tcp_close_state
->tcp_set_state(sk, new_state[TCP_ESTABLISHED]),也就是TCP_FIN_WAIT1B、发送FIN包
tcp_close->tcp_close_state
->tcp_send_fin2、服务端收到FIN包,状态从TCP_ESTABLISHED变为TCP_CLOSE_WAIT,并返回ACK包
A、状态变为TCP_CLOSE_WAIT
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_established
->tcp_data_queue
->->tcp_fin
->->->inet_csk_schedule_ack; 安排ack
->->->sk->sk_shutdown |= RCV_SHUTDOWN; 模拟了close
->->->sock_set_flag(sk, SOCK_DONE);
->->->case TCP_ESTABLISHED:
->->->tcp_set_state(sk, TCP_CLOSE_WAIT); 修改状态
->->inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_NOW; ACS是否立即发送B、发送ACK包
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_established【接上面】
->tcp_ack_snd_check->__tcp_ack_snd_check->tcp_send_ack3、客户端收到ACK包,状态从TCP_FIN_WAIT1变为TCP_FIN_WAIT2,然后被替换为状态TCP_TIME_WAIT,子状态TCP_FIN_WAIT2
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process
->case TCP_FIN_WAIT1:
->tcp_set_state(sk, TCP_FIN_WAIT2);
->tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
->->tw = inet_twsk_alloc(sk, tcp_death_row, state);
->->->tw->tw_state = TCP_TIME_WAIT;
->->->tw->tw_substate = TCP_FIN_WAIT2;
->->->timer_setup(&tw->tw_timer, tw_timer_handler, TIMER_PINNED);4、服务端状态从TCP_CLOSE_WAIT变为TCP_LAST_ACK,发送FIN包
A、状态变为TCP_LAST_ACK
tcp_close->tcp_close_state
->tcp_set_state(sk, new_state[TCP_CLOSE_WAIT]),也就是TCP_LAST_ACKB、发送FIN包
2021-08-15
tcp_close->tcp_close_state
->tcp_send_fin - neohope 👍(6) 💬(1)
三次握手过程分析【V5.8,正常流程】
1、客户端发起第一次握手,状态调变为TCP_SYN_SENT,发送SYN包
connect->__sys_connect->__sys_connect_file->【sock->ops->connect】tcp_v4_connect
A、状态变化
->tcp_set_state(sk, TCP_SYN_SENT);
B、发送SYN
->tcp_connect->tcp_send_syn_data2、服务端收到客户端的SYN包,初始化socket,状态从TCP_LISTEN变为TCP_NEW_SYN_RECV,发送第二次握手SYN_ACK包
A、收到连接,初始化socket
accept->__sys_accept4->__sys_accept4_file->【sock->ops->accept】inet_csk_acceptB、收到SYN,改变状态
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process->
->case TCP_LISTEN:
->[sock->ops->conn_request]tcp_v4_conn_request->tcp_conn_request
->->inet_reqsk_alloc
->->->ireq->ireq_state = TCP_NEW_SYN_RECV;C、发送SYN_ACK包
->[sock->ops->conn_request]tcp_v4_conn_request->tcp_conn_request【和B路径一样】
->->【af_ops->send_synack】tcp_v4_send_synack
->->->tcp_make_synack
->->->__tcp_v4_send_check3、客户端收到SYN_ACK包,状态从TCP_SYN_SENT变为TCP_ESTABLISHED,并发送ACK包
A、收到SYN_ACK包
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process
->case TCP_SYN_SENT:
->tcp_rcv_synsent_state_process->tcp_finish_connect
->->tcp_set_state(sk, TCP_ESTABLISHED);B、发送ACK包
->tcp_rcv_synsent_state_process->tcp_send_ack->__tcp_send_ack4、服务端收到ACK包,状态从TCP_NEW_SYN_RECV变为TCP_SYN_RECV【实际上是新建了一个sock】
【tcp_protocol.handler】tcp_v4_rcv->
->if (sk->sk_state == TCP_NEW_SYN_RECV)
->tcp_check_req
->->【inet_csk(sk)->icsk_af_ops->syn_recv_sock】tcp_v4_syn_recv_sock->tcp_create_openreq_child->inet_csk_clone_lock
->->->inet_sk_set_state(newsk, TCP_SYN_RECV);5、服务端状态从TCP_SYN_RECV变为TCP_ESTABLISHED
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process
->case TCP_SYN_RECV:
->tcp_set_state(sk, TCP_ESTABLISHED);只看没调,有问题的欢迎各位小伙伴指出。
2021-08-14 - pedro 👍(2) 💬(3)
今天的问题不好回答,因为文中无明显三次握手的代码,而且三次握手的机制其实比较复杂,涉及到几个状态和几个队列之间的切换,笼统的 connect 和 accept 函数是说不清楚的,感兴趣可以看看这里:
https://blog.csdn.net/tennysonsky/article/details/45621341当然这些不能全信,所以还是得自己看linux内核代码,待我看了再来补充😂
2021-08-09 - 王子虾 👍(0) 💬(1)
老师,有一个问题,tcp在调用listen的时候,有全连接队列的概念,一般上限是128。但是问题是,我们比如实现单机百万链接的时候,一个server端的源组(server_ip+port),比如有65535个client,那会不会受限于这个全连接队列?
2022-04-09 - ifelse 👍(0) 💬(1)
nice
2022-02-25 - GeekCoder 👍(0) 💬(1)
能讲讲epoll吗?
2021-10-24 - pedro 👍(0) 💬(1)
这里有一篇三次握手的源码图解:https://mp.weixin.qq.com/s/vlrzGc5bFrPIr9a7HIr2eA
2021-08-09 - MacBao 👍(1) 💬(0)
服务器端处于listen状态,客户端connect发起TCP三次握手?
2021-08-09