1.socket函数
int socket(int protofamily, int type, int protocol);//返回sockfd,描述符
protofamily:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。
注意:
并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口
实现
当服务器程序调用socket系统调用之后,内核会创建一个struct socket和一个struct sock结构,两者可以通过指针成员变量相互访问对方。内核直接操作的是struct sock结构。struct socket的存在是为了适应linux的虚拟文件系统,把socket也当作一个文件系统,通过指定superblock中不同的操作函数实现完成相应的功能。在linux内核中存在不同的sock类型,与TCP相关的有struct sock、 struct inet_connection_sock,、struct tcp_sock等。这些结构的实现非常灵活,可以相互进行类型转换。这个机制的实现是结构体的一层层包含关系:struct tcp_sock的第一个成员变量是struct inet_connection_sock,struct inet_connection_sock的第一个成员变量是struct sock。
通过这种包含关系,可以将不同的sock类型通过指针进行相互转换。比如:
struct tcp_sock tcp_sk; struct sock *sk = (struct sock *)&tcp_sk;
为了避免从小的结构体转换到大的结构体造成内存越界,对于TCP协议,内核在初始化一个stuct sock时给它分配的空间大小是一个struct tcp_sock的大小。这样sock类型的相互转换便可以灵活的进行。另外,在内核创建完sock和socket之后,还需要绑定到对应的文件描述符以便应用层能够访问。一个task_struct中有一个文件描述符数组,存储所有该进程打开的文件,因为socket也可以看做是文件,也存储在这个数组中。文件描述符就是该socket在该数组中的下标,具体的实现请参照虚拟文件系统。
2.bind函数
bind()函数把一个地址族中的特定地址(本地协议地址)赋给socket(即地址的绑定)。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是: 限定了只接受地址为addr的客户信息,若服务器没有绑定ip地址,则内核就把客户端发送的SYN目的地址作为服务器的源IP地址,一般我们捆绑统配地址:INADDR_ANY,告诉系统,若系统是多宿主机,我们将接受目的地址为任何本地接口的连接。
addrlen:对应的是地址的长度。
错误信息:
- EACCES:地址受到保护,用户非超级用户。
- EADDRINUSE:指定的地址已经在使用。
- EBADF:sockfd参数为非法的文件描述符。
- EINVAL:socket已经和地址绑定。
- ENOTSOCK:参数sockfd为文件描述符。
注意:
- 如果TCP客户或服务器未曾调用bind捆绑一个端口,当调用connect或listen时内核就选择一个临时端口,这对客户来说是正常的,服务器应该调用众所周知的端口
- 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,务必将其转化为网络字节序再赋给socket
实现
该调用通过传递进来的文件描述符找到对应的socket结构,然后通过socket访问sock结构。操作sock进行地址的绑定。如果指定了端口检查端口的可用性并绑定,否则随机分配一个端口进行绑定。但是怎样获知当前系统的端口绑定状态呢?通过一个全局变量inet_hashinfo进行,每次成功绑定一个端口会都将该sock加入到inet_hashinfo的绑定散列表中。加入之后bind的系统调用已基本完成了。
3.listen函数
int listen(int sockfd, int backlog);
- 第一个参数即为要监听的socket描述字
- 第二个参数为相应socket可以排队的最大连接个数
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求,把CLOSED状态转换为LISTEN状态。
未完成队列:
每个这样的SYN分节对应其中一项,客服发送至服务器,服务器等待完成TCP的三次握手,这些套接字处于SYN_RCVD状态。
已完成队列:
每个已完成的TCP完成三路握手的客户对应其中一项,这些套接字处于ESTABLISHED状态。
注意:
- 每在未完成队列中创建一项时,来自监听套接字的参数就立即复制到建立连接中,链接创建自动完成。
- 来自客户的SYN到达时,TCP在未完成对队列中创建一项,然后相应三路握手的第二个分节:服务器SYN相应,捎带对客户的SYN分节的ACK,这一项一直保留在未完成队列中,直到三路握手的第三个分节客户对服务器的SYN的ACK到达或该项超时为止。
- 已完成队列的对头返回给进程,如果进程为空,队列被投入睡眠,直到TCP在该队列中放一项为止;若当客户的一个SYN到达时,这些队列是满的,TCP就忽略该分节,也就是不发送RST,因为这些情况是暂时的,期望不就就能在这些队列中找到一个可用的空间,若服务器响应RST,客户端connect调用就会返回一个错误。
- backlog不能为0
实现
和listen相关的大部分信息存储在inet_connection_sock结构中。同样的内核通过文件描述符找到对应的sock,然后将其转换为inet_connection_sock结构。在inet_connection_sock结构体中含有一个类型为request_sock_queue的icsk_accept_queue变量,存储一些希望建立连接的sock相关的信息。结构为:
struct request_sock_queue { struct request_sock *rskq_accept_head; struct request_sock *rskq_accept_tail; rwlock_t syn_wait_lock; u8 rskq_defer_accept; struct listen_sock *listen_opt; };