【Unix 网络编程】说说 socket 套接字

时间:2022-06-21 11:00:10

套接字描述符同文件描述符一样是一个整数类型的值,是通信端点的抽象。对于每个程序系统都有单独的表,精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者。为了将不同类型的 IO 与对应的文件描述符绑定,则需要不同的初始化函数,这相当于面对对象里面的多态。


socket 是 inode 结构中的一部分,在以前的内核版本中,把 inode 结构内部的一个 union 用作 socket 结构,新的内核中的 inode 节点中已经没有 u 这个联合体了,对应的是一个包含 socket 和 inode 的一个结构体。


普通文件就通过 open 函数,指定对应的文件路径,操作系统通过路径能够找到对应的文件系统类型,当应用程序读取该文件时,只需知道该文件描述符,然后操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。对于网络,则通过 socket 函数来初始化,socket 函数就通过(domain, type, protocol)来找到对应的网络协议栈。套接字描述和文件描述符在本质上是一样的,秉承 Linux 一切皆文件的设计原则。


在系统调用时,和文件描述符一样,套接字描述符会被映射成一个表示 socket 的结构体,该结构体保存了该 socket 的所有属性和数据。

这里我们主要看看 socket 对象记录了哪些信息

//include/linux/net.h   Linux-2.6.32.61
struct socket {
socket_statestate;

kmemcheck_bitfield_begin(type);
shorttype;
kmemcheck_bitfield_end(type);

unsigned longflags;
struct fasync_struct*fasync_list;
wait_queue_head_twait;

struct file*file;
struct sock*sk;
const struct proto_ops*ops;
};
上面是 socket 结构体,我们主要关心里面的 struct sock 和 struct proto_ops

【Unix 网络编程】说说 socket 套接字

struct proto_ops 是协议相关的一组操作集,里面包含了几乎所有的协议操作函数,所以 socket 描述符可以发起各类操作函数

struct proto_ops {
intfamily;
struct module*owner;
int(*release) (struct socket *sock);
int(*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);
int(*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags);
int(*socketpair)(struct socket *sock1, struct socket *sock2);
int(*accept) (struct socket *sock, struct socket *newsock, int flags);
int(*getname) (struct socket *sock, struct sockaddr *addr, int *sockaddr_len, int peer);
unsigned int(*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait);
int(*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg);
int(*compat_ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg);
int(*listen) (struct socket *sock, int len);
int(*shutdown) (struct socket *sock, int flags);
int(*setsockopt)(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen);
int(*getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen);
int(*compat_setsockopt)(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen);
int(*compat_getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen);
int(*sendmsg) (struct kiocb *iocb, struct socket *sock, struct msghdr *m, size_t total_len);
int(*recvmsg) (struct kiocb *iocb, struct socket *sock, struct msghdr *m, size_t total_len, int flags);
int(*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma);
ssize_t(*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags);
ssize_t(*splice_read)(struct socket *sock, loff_t *ppos, struct pipe_inode_info *pipe, size_t len, unsigned int flags);
};

struct sock 是根据使用的协议而挂入 socket ,每一种协议都有此结构变量,将 sock 从socket 中分离出来是因为 socket  是通用的套接字结构体,而 sock 与具体使用的协议相关,把与文件系统关系密切的放在 socket 结构中,把与通信关系密切的放在 sock 中。总而言之,公共的通用部分放在 socket 结构中,而通用部分的放在 sock 结构体中,还有专用部分则是具体协议族使用的结构,如 inet_sock 结构体。

sock 结构体定义内容相对庞大,这里只贴出局部,具体参见源码

//include/net/sock.h
struct sock {

struct sock_common__sk_common; //与 inet_timewait_sock 共享使用
#define sk_node__sk_common.skc_node
#define sk_nulls_node__sk_common.skc_nulls_node
#define sk_refcnt__sk_common.skc_refcnt

#define sk_copy_start__sk_common.skc_hash
#define sk_hash__sk_common.skc_hash
#define sk_family__sk_common.skc_family
……
};
【Unix 网络编程】说说 socket 套接字

由上面可知,socket 与 sock 是互指的关系

还有专用的 socket 如 inet_sock 是INET域专用的一个socket,它是在 struct sock 的基础上进行的扩展,在基本 socket 的属性已具备的基础上,struct inet_sock 提供了INET域专有的一些属性。

struct inet_sock {
/* sk and pinet6 has to be the first two members of inet_sock */
struct socksk; //sock 结构体
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
struct ipv6_pinfo*pinet6;
#endif
/* Socket demultiplex comparisons on incoming packets. */
__be32daddr; //IPv4的目的地址(远程地址)
__be32rcv_saddr; //IPv4的本地接收地址
__be16dport; //目的端口(远程)
__u16num; //本地端口(主机字节序)
__be32saddr;

……
};

这里的 inet_sock 结构体不仅记录了本地的IP和端口号,还记录了目的IP和端口号。

【Unix 网络编程】说说 socket 套接字

可以直接将 sock 结构体变量强制转换为 inet_sock 结构体变量,因为在分配 sock 结构体变量时,真正分配的是 tcp_sock(TCP,udp_sock(UDP)),由于 tcp_sock、inet_sock。sock 之间均为0处偏移量,因此可以直接将 tcp_sock 强制转换为 inet_sock。这类似于C++面向对象中的 is-a 思想。

struct tcp_sock {
/* inet_connection_sock has to be the first member of tcp_sock */
struct inet_connection_sockinet_conn;
u16tcp_header_len;/* Bytes of tcp header to send*/
……
};

struct inet_connection_sock {
/* inet_sock has to be the first member! */
struct inet_sock icsk_inet;
struct request_sock_queue icsk_accept_queue;
……
};

struct inet_sock {
/* sk and pinet6 has to be the first two members of inet_sock */
struct socksk;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
struct ipv6_pinfo*pinet6;
#endif
……
};

其内存布局大致如下图(图片来源于网络)

【Unix 网络编程】说说 socket 套接字

inet_connection_sock 是所有面向连接的协议的 socket 的相关信息,tcp_sock 是TCP协议专用的一个 socket 表示,它是在 inet_connection_sock 基础上进行的扩展。

这里只是简单的介绍了一下 socket 套接字,其余具体需要跟踪 socket 的创建才能更深的了解。