我们都知道在网络编程中,可以调用socket()函数来获取一个套接字描述符。但是,socket()函数的作用只是返回一个很小的非负整数值吗?并非如此,socket()函数的作用是创建套接字的。
创建套接字的流程如下
socket()函数会触发内核调用sys_socket()函数,然后sys_socket()函数会调用sock_create()函数。此函数
会根据我们在socket()函数的参数中指定的协议族类型调用不同的套接字创建函数。例如,如果在socket()
函数的参数表中指定协议族为PF_INET时,sock_create()函数会调用inet_create()函数创建INFT套接字。
这些套接字函数首先创建套接字的内核表示结构,在返回一个套接字描述符来标识生成的套接字对象。
因此,套接字是有内核表示的结构的,但是我们的应用程序只需要获取其描述符即可。并且在这里可以看出,这里的套接字结构十分简单,没有过多的信息。但是我们知道很多协议通信中,我们需要指定一些ip地址和端口号来创建连接。很明显,这里的套接字结构中并不包含这些信息。因此,我们在进行网络通信的过程中,需要将由socket()函数创建的套接字与一个地址相关联,这个地址包括ip地址和端口号等信息,这个地址我们使用套接字地址结构来表示。不同协议族的套接字地址结构也不相同,下面是IPv4和IPv6的套接字地址结构:
//IPv4
struct sockaddr_in
{
unsigned short sin_len; //IPv4地址长度
short int sin_family; //指代协议簇,在TCP套接字编程只能是AF_INET
unsigned short sin_port; //存储端口号(使用网络字节顺序),数据类型是一个16为的无符号整形类型
struct in_addr sin_addr;//存储IP地址,IP地址是一个in_add结构体(结构在下面)
unsigned char sin_zero[8]; //为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
};
struct in_addr
{
unsigned long s_addr; //按照网络字节顺序存储IP地址
};
//IPv6
struct sockaddr_in6
{
unsigned short int sin6_len; //IPv6结构长度,是一个无符号的8为整数,表示128为IPv6地址长度
short int sin6_family; //地址类型AF_INET6
unsigned short int sin6_port; //存储端口号,按网络字节顺序
unsigned short int sin6_flowinfo; //低24位是流量标号,然后是4位的优先级标志,剩下四位保留
struct in6_addr sin6_addr; //IPv6地址,网络字节顺序
};
struct in6_addr
{
unsigned long s6_addr; //128位的IPv6地址,网络字节顺序
};
struct sockaddr
{
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};
好了,现在套接字有了,套接字地址也有了,我们需要将这两者进行绑定,这个过程就是通过bind()函数来实现的。bind()函数的原型如下:
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
struct sockaddr_in serv;
/***/
bind(sockfd, (struct sockaddr *) &serv, sizeof(serv));
注意理解这里用来绑定套接字描述符的套接字地址结构和客户端用来connect服务器的套接字描述符(connect()函数的第二个参数)。bind()是绑定的地址结构是本机的端口号,因此在TCP通信过程中,在客户端connect()函数之前绑定似乎没有什么意义。即,在客户端绑定套接字地址的前提是在与服务器建立连接之前就知道客户端本机将要进行通信工作的端口从而指定该端口来通讯。但是这样容易使得不同的某台主机上面不同的客户端进程争夺一个端口从而发生抢占。如果将分配端口的工作交给内核,内核会为没有绑定端口的套接字分配一个未被占用的端口。
具体可以参考这篇博文http://blog.chinaunix.net/uid-23193900-id-3199173.html