1.socket初探
2.socket分析
3.socket内核源码分析
//1.生成内核socket;2。与文件描述符绑定
socket(AF_UNIX, SOCK_STREAM, 0);
//建立连接,包含三次握手
connect(sockfd, (struct sockaddr *)&address, len);
//绑定一个IP地址和端口到socket套接字上
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
//半连接队列、全连接队列
int listen(int sockfd, int backlog)
//返回一个new的socket文件描述符(不占用端口号)
accept(server_sockfd,(struct sockaddr *)&client_address, client_len);
//断开连接,包含四次挥手(TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。TIME_WAIT原因)
int close(int sockfd);
//可选择性的断开连接
系统调用的过程:
1.int socket(int domain,int type,int protocol)
作用:根据用户定义的网络类型、协议类型、和具体的协议标号,生成一个套接字文件描述符供用户使用,实现各种初始化工作(文件系统初始化、socket初始化等)
- //[sock_create]
- [ ] 分配socket结构:1.在socket文件系统中创建i节点;2.创建socket专用inode;
- [ ] 根据inode取得socket对象:
- [ ] 使用协议族来初始化socket:1) 注册AF_INET协议域 2)套接字类型(如AF_INET域下存在流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW),在这三种类型的套接字上建立的协议分别是TCP, UDP,ICMP/IGMP);3) 使用协议域来初始化socket
- [ ] 分配sock结构:
- [ ] 建立socket结构与sock结构的关系:
- [ ] 使用tcp协议初始化sock:
- //[sock_map_fd]
- [ ] socket与文件系统关联;
2.int bind(int sockfd,const struct sockaddr *my_addr,socklen_t addrlen)
- [ ] bind()的Socket层实现
- [ ] bind()的tcp层实现、端口的冲突处理
- Q: 什么情况下会出现冲突呢?
同时符合以下条件才会冲突:
绑定的设备相同(不允许自动选择设备)
绑定的IP地址相同(不允许自动选择IP)
3 以下条件有一个成立:
3.1 要绑定的socket不允许重用 3.2 已绑定的socket不允许重用 3.3 已绑定的socket处于监听状态 3.4 relax参数为false
端口区间(0--65535)
我们可以指定系统自动分配端口号时,端口的区间:
/proc/sys/net/ipv4/ip_local_port_range,默认为:32768 61000
也可以指定要保留的端口区间:
/proc/sys/net/ipv4/ip_local_reserved_ports,默认为空
系统自动选择端口时:不优先选择没被使用过的端口。只要没有冲突,直接重用端口。
- 一个网络应用程序只能绑定一个端口( 一个套接字只能 绑定一个端口 )
- 一般情况下服务器需要绑定端口号,而客户端可以不绑定端口号,在send的时候,系统随机分配一个端口号。
- 端口复用技术//设置socket的SO_REUSEADDR选项,即可实现端口复用
- SO_REUSEADDR可以用在以下四种情况下。 (摘自《Unix网络编程》卷一,即UNPv1)
1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。
2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可以测试这种情况。
3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。这和2很相似,区别请看UNPv1。
4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP
- 端口复用最常用的用途应该是防止服务器重启时之前绑定的端口还未释放或者程序突然退出而系统没有释放端口
- 当在一个应用或是进程中多个socket同时绑定到相同的端口时,这些套接字并不是所有都能读取信息,只有最后一个套接字会正常接收数据。
浅析套接字中SO_REUSEPORT和SO_REUSEADDR的区别
3.int listen(int sockfd,int backlog)
backlog的定义
Now it specifies the queue length for completely established sockets waiting to be accepted,instead of the number of incomplete connection requests. The maximum length of the queuefor incomplete sockets can be set using the tcp_max_syn_backlog sysctl. When syncookiesare enabled there is no logical maximum length and this sysctl setting is ignored.If the socket is of type AF_INET, and the backlog argument is greater than the constant SOMAXCONN(128 default), it is silently truncated to SOMAXCONN.
全连接队列的最大长度:
- backlog保存的是完成三次握手、等待accept的全连接队列
- 负载不高时,backlog不用太大。(For complete connections)
- 系统最大的、未处理的全连接数量为:min(backlog,somaxconn),net.core.somaxconn默认为128。这个值最终存储于sk->sk_max_ack_backlog
半连接队列的最大长度:
- tcp_max_syn_backlog默认值为256。(For incomplete connections)
- 当使用SYN Cookie时,这个参数变为无效。
- 半连接队列的最大长度为backlog、somaxconn、tcp_max_syn_backlog的最小值。
- 检查套接口的状态、当前连接的状态是否合法,然后调用inet_csk_listen_start()启动监听。
- 启动监听时,做的工作主要包括:
创建半连接队列的实例,初始化全连接队列。
初始化sock的一些变量,把它的状态设为TCP_LISTEN。
检查端口是否可用,防止bind()后其它进程修改了端口信息。
把sock链接进入监听哈希表listening_hash中。
- listen_sock结构用于保存SYN_RECV状态的连接请求块,所以也叫半连接队列
- 销毁连接请求块中的listen_sock实例,释放半连接队列
- inet_hash()用于把sock链入监听哈希表listening_hash,或者已建立连接的哈希表ehash。
4.int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
It extracts the first connection request on the queue of pending connections (backlog), creates a newconnected socket, and returns a new file descriptor referring to that socket.If no pending connections are present on the queue, and the socket is not marked as non-blocking,accept() blocks the caller until a connection is present. If the socket is marked non-blocking and no pending connections are present on the queue, accept() fails with the error EAGAIN.
- 在sys_socketcall()中会调用sys_accept4():
- 创建了一个新的socket和inode,以及它所对应的fd、file。
- 调用Socket层操作函数inet_accept()。
- 保存对端地址到指定的用户空间地址
- SOCK_STREAM套接口的Socket层操作函数集实例为inet_stream_ops,连接接收函数为inet_accept():
- 调用TCP层的操作函数,获取已建立的连接sock。
- 把新socket和sock关联起来。
- 把新socket的状态设为SS_CONNECTED。
- SOCK_STREAM套接口的TCP层操作函数集实例为tcp_prot,其中连接接收函数为inet_csk_accept().inet_csk_accept()用于从backlog队列(全连接队列)中取出一个ESTABLISHED状态的连接请求块,返回它所对应的连接sock,同时更新backlog队列的全连接数,释放取出的连接控制块.
- 非阻塞的,且当前没有已建立的连接,则直接退出,返回-EAGAIN。
- 阻塞的,且当前没有已建立的连接:
2.1 用户没有设置超时时间,则无限期阻塞。
2.2 用户设置了超时时间,超时后会退出。
accept()是如何避免惊群现象(当内核接收到一个客户连接后,只会唤醒等待队列上的第一个进程或线程)的:
初始化等待任务时,flags|=WQ_FLAG_EXCLUSIVE。传入的nr_exclusive为1,表示只允许唤醒一个等待任务。
所以这里只会唤醒一个等待的进程,不会导致惊群现象。
Nginx中使用mutex互斥锁解决这个问题,具体措施有使用全局互斥锁,每个子进程在epoll_wait()之前先去申请锁,申请到则继续处理,获取不到则等待,并设置了一个负载均衡的算法(当某一个子进程的任务量达到总设置量的7/8时,则不会再尝试去申请锁)来均衡各个进程的任务量。使用mutex锁住多个线程是不会惊群的,在某个线程解锁后,只会有一个线程会获得锁,其它的继续等待.
5.int connect(int sockfd,struct sockaddr *,int addrlen)
- SOCK_STREAM套接口的socket层操作函数集实例为inet_stream_ops,其中主动建立连接的函数为inet_stream_connect()。
检查socket地址长度和使用的协议族。
检查socket的状态,必须是SS_UNCONNECTE或SS_CONNECTING。
调用tcp_v4_connect()来发送SYN包。
-
等待后续握手的完成:
如果socket是非阻塞的,那么就直接返回错误码-EINPROGRESS。
如果socket为阻塞的,就调用inet_wait_for_connect(),通过睡眠来等待。在以下三种情况下会被唤醒:
(1) 使用SO_SNDTIMEO选项时,睡眠时间超过设定值,返回0。connect()返回错误码-EINPROGRESS。
(2) 收到信号,返回剩余的等待时间。connect()返回错误码-ERESTARTSYS或-EINTR。
(3) 三次握手成功,sock的状态从TCP_SYN_SENT或TCP_SYN_RECV变为TCP_ESTABLISHED,sock I/O事件的状态变化处理函数sock_def_wakeup()就会唤醒进程。connect()返回0。
进程的睡眠: connect()的超时时间为sk->sk_sndtimeo,在sock_init_data()中初始化为MAX_SCHEDULE_TIMEOUT,表示无限等待,可以通过SO_SNDTIMEO选项来修改。
进程的唤醒:三次握手中,当客户端收到SYNACK、发出ACK后,连接就成功建立了。此时连接的状态从TCP_SYN_SENT变为TCP_ESTABLISHED,sock的状态发生变化,会调用sock_def_wakeup()来处理连接状态变化事件,唤醒进程,connect()就能成功返回了。
close()与shutdown()
int close(int sockfd); //返回成功为0,出错为-1.
int shutdown(int sockfd,int howto); //返回成功为0,出错为-1.
1.SHUT_RD:值为0,关闭连接的读这一半。
2.SHUT_WR:值为1,关闭连接的写这一半。
3.SHUT_RDWR:值为2,连接的读和写都关闭。
close函数会关闭套接字ID,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,并且有时候这是非常重要的,特别是对于多进程并发服务器来说。在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共享着的进程个数,当父进程或某一子进程close掉套接字时,描述符引用计数会相应的减一,当引用计数仍大于零时,这个close调用就不会引发TCP的四路握手断连过程。
shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零,那些试图读得进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号,同时可利用shutdown的第二个参数选择断连的方式。利用shutdown()可以避免用close()过程出现死锁现象
//close()
/* First Sample client fragment,
* 多余的代码及变量的声明已略 */
s=connect(...);
if( fork() ){ /* The child, it copies its stdin to the socket */
while( gets(buffer) >0)
write(s,buf,strlen(buffer));
close(s);
exit(0);
}
else { /* The parent, it receives answers */
while( (n=read(s,buffer,sizeof(buffer)){
do_something(n,buffer);
/* Connection break from the server is assumed */
/* ATTENTION: deadlock here */
wait(0); /* Wait for the child to exit */
exit(0);
}
//shutdown()
if( fork() ) { /* The child */
while( gets(buffer)
write(s,buffer,strlen(buffer));
shutdown(s,1); /* Break the connection
*for writing, The server will detect EOF now. Note: reading from
*the socket is still allowed. The server may send some more data
*after receiving EOF, why not? */
exit(0);
}