在网络编程中经常使用的就是socket来进行两台机器间的通信。在自己的项目里还用上了EPOLL来监听socket的缓冲区信息变化。
但是socket连接和通信与网络连接的关系是怎么样的呢?接下来就来看看。
Socket API
首先看socket的API:
socket
socket(int domain, int type, int protocol)
man socket :
socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint. The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process.
socket()创建用于通信的端点,并返回引用该端点的文件描述符。文件
成功调用返回的描述符将是进程当前未打开的编号最低的文件描述符。
通过描述我们可以得知,这就是用来创建一个通话端口的API,相当于给了我们一个手机,可以用来接电话,也能用来打电话。
我们还可以看到它有三个参数,
- domain参数指定一个通信域;它选择将用于通信的协议族。这些族在<sys/socket.h>中定义。目前理解的格式包括:
AF_UNIX, AF_LOCAL Local communication 本地通信
AF_INET IPv4 Internet protocols IPv4
AF_INET6 IPv6 Internet protocols IPv6
一般现在用到的就这几个,还有别的可以通过man查看。
- 套接字具有指定的类型type,该类型指定通信语义。当前定义的类型包括:
SOCK_STREAM:
提供有序、可靠、双向、基于连接的字节流。带外数据传输可能支持该机制。
SOCK_DGRAM:
支持数据报(固定最大长度的无连接、不可靠的消息).
经常用的就这两个,别的可以通过man查看:
协议指定要与套接字一起使用的特定协议。通常只有一个协议支持端口给定协议族中的特定套接字类型,在这种情况下,可以将协议指定为0。
bind
man bind
When a socket is created with socket(2), it exists in a name space (address family) but has no address assigned to it.
bind() assigns the address specified by addr to the socket referred to by the file descriptor sockfd. addrlen specifie the size, in bytes, of the address structure pointed to by addr. Traditionally, this operation is called “assigning a name to a socket”.
使用套接字(2)创建套接字时,它存在于名称空间(地址族)中,但没有分配给它的地址。bind()将addr指定的地址分配给文件描述符sockfd引用的套接字。addrlen指定addr指向的地址结构的大小(字节)。传统上,这种操作称为“为套接字指定名称”。
所以就是用来绑定本地的端口号和地址的,相当于电话有了电话卡,有确定的号码了。
那首先肯定要先获得电话卡,在这里是 sockaddr。
直接上例子
// 设置服务器IP和Port,和监听描述副绑定
struct sockaddr_in server_addr;
bzero((char*)&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons((unsigned short)port);
然后调用bind,传入要绑定的socket和地址,最后是地址的大小。
if(bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
return -1;
listen
man listen
listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).
listen()将sockfd引用的套接字标记为被动套接字,即用于接受的套接字使用accept(2)的传入连接请求。
backlog参数目前没啥用了,可以忽略,指的是sockfd的挂起连接队列可能增长到的最大长度,也就是等待连接的数目。
// 开始监听,最大等待队列长为LISTENQ
if(listen(listen_fd, LISTENQ) == -1)
return -1;
!!!各单位注意!!!三次握手在什么时候进行的?
这里直接参考大佬的图,相关博文在参考文章中。
也及时listen时就进行了三次握手了,accept只是把就绪的socket给取出来。
在进程/线程(监听者)监听的过程中,它阻塞在select()或poll()上。直到有数据(SYN信息)写入到它所监听的sockfd中(即recv buffer),内核被唤醒(注意不是app进程被唤醒,因为TCP三次握手和四次挥手是在内核空间由内核完成的,不涉及用户空间)并将SYN数据拷贝到kernel buffer中进行一番处理(比如判断SYN是否合理),并准备SYN+ACK数据,这个数据需要从kernel buffer中拷入send buffer中,再拷入网卡传送出去。这时会在连接未完成队列(syn queue)中为这个连接创建一个新项目,并设置为SYN_RECV状态。然后再次使用select()/poll()方式监控着套接字listenfd,直到再次有数据写入这个listenfd中,内核再次被唤醒,如果这次写入的数据是ACK信息,表示是某个客户端对服务端内核发送的SYN的回应,于是将数据拷入到kernel buffer中进行一番处理后,把连接未完成队列中对应的项目移入连接已完成队列(accept queue/established queue),并设置ESTABLISHED状态,如果这次接收的不是ACK,则肯定是SYN,也就是新的连接请求,于是和上面的处理过程一样,放入连接未完成队列。对于已经放入已完成队列中的连接,将等待内核通过accept()函数进行消费(由用户空间进程发起accept()系统调用,由内核完成消费操作),只要经过accept()过的连接,连接将从已完成队列中移除,也就表示TCP已经建立完成了,两端的用户空间进程可以通过这个连接进行真正的数据传输了,直到使用close()或shutdown()关闭连接时的4次挥手,中间再也不需要内核的参与。这就是监听者处理整个TCP连接的循环过程。
ACCEPT
accept()系统调用用于基于连接的套接字类型(SOCK\ u STREAM、SOCK\ u SEQPACKET)。它提取第一个侦听套接字的挂起连接队列上的连接请求sockfd创建一个新的已连接套接字,并返回引用该套接字的新文件描述符。新创建的套接字未处于侦听状态。这个原始套接字sockfd不受此调用的影响。
这过程没有三次握手。
close
关闭本进程(自身)的socket id,但连接还是未断开的,用这个socket id的其它进程还能用这个连接,能读或写这个socket id。使用close中止一个连接,但它只是减少描述符的参考数,并不直接关闭连接,只有当描述符的参考数为0时才关闭连接。
在多进程的并发场景,假设客户端有两个进程(注意是客户端),父进程和子进程,子进程是在父进程和服务器建立连接之后fork出来的,因此客户端的socket id的引用次数+1。
我们期望实现这样的功能:
(1)子进程将数据写入套接字后close,并退出。
(2)服务端接收完数据,直到检测到EOF(接收到FIN标志),关闭连接并退出。
(3)父进程读取完服务端响应的数据,也退出。
如果子进程使用close的话,并不会发生4次挥手的过程,只是引用计数减1,服务端是接收不到EOF的(客户端是不会发送FIN的),这时就需要使用优雅关闭了。
shutdown
shutdown 可以选择关闭某个方向或者同时关闭两个方向,shutdown how = 0 or how = 1 or how = 2 (SHUT_RD or SHUT_WR or SHUT_RDWR),后两者可以保证对等方接收到一个EOF字符(即发送了一个FIN段),而不管其他进程是否已经打开了这个套接字。而close不能保证,只有当某个sockfd的引用计数为0,close 才会发送FIN段,否则只是将引用计数减1而已。也就是说只有当所有进程(可能fork多个子进程都打开了这个套接字)都关闭了这个套接字,close 才会发送FIN 段。
所以说,如果是调用shutdown how = 1 ,则意味着往一个已经发送出FIN的套接字中写是允许的,接收到FIN段仅代表对方不再发送数据,但对方还是可以读取数据的,可以让对方可以继续读取缓冲区剩余的数据。