socket() 函数用来创立套接字,肯定套接字的各类属性,然后效劳器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只要如许,流经该IP地址和端口的数据才干交给套接字处置;而客户端要用 connect() 函数树立衔接。

bind() 函数

bind() 函数的原型为:

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows
下面以Linux为例停止解说,Windows与此相似。

sock 为 socket 文件描绘符,addr 为 sockaddr 构造体变量的指针,addrlen 为 addr 变量的巨细,可由 sizeof() 盘算得出。
下面的代码,将创立的套接字与IP地址 127.0.0.1、端口 1234 绑定:

			//创立套接字 int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创立sockaddr_in构造体变量 struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); //每一个字节都用0填充 serv_addr.sin_family = AF_INET; //运用IPv4地址 serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //详细的IP地址 serv_addr.sin_port = htons(1234); //端口 //将套接字和IP、端口绑定 bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

这里我们运用 sockaddr_in 构造体,然后再强迫转换为 sockaddr 类型,后边会解说为什么如许做。

sockaddr_in 构造体

接下来无妨先看一下 sockaddr_in 构造体,它的成员变量如下:

			struct sockaddr_in{ sa_family_t sin_family; //地址族(Address Family),也就是地址类型 uint16_t sin_port; //16位的端标语 struct in_addr sin_addr; //32位IP地址 char sin_zero[8]; //不运用,普通用0填充 };

1) sin_family 和 socket() 的第一个参数的寄义相反,取值也要坚持分歧。
2) sin_prot 为端标语。uint16_t 的长度为两个字节,实际上端标语的取值规模为 0~65536,但 0~1023 的端口普通由零碎分派给特定的效劳程序,例如 Web 效劳的端标语为 80,FTP 效劳的端标语为 21,所以我们的程序要尽量在 1024~65536 之间分派端标语。
端标语需求用 htons() 函数转换,前面会解说为什么。
3) sin_addr 是 struct in_addr 构造体类型的变量,下面会具体解说。
4) sin_zero[8] 是过剩的8个字节,没有效,普通运用 memset() 函数填充为 0。下面的代码中,先用 memset() 将构造体的全体字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 天然就是 0 了。

in_addr 构造体

sockaddr_in 的第3个成员是 in_addr 类型的构造体,该构造体只包括一个成员,如下所示:

			struct in_addr{ in_addr_t s_addr; //32位的IP地址 };

in_addr_t 在头文件 <netinet/in.h> 中界说,等价于 unsigned long,长度为4个字节。也就是说,s_addr 是一个整数,而IP地址是一个字符串,所以需求 inet_addr() 函数停止转换,例如:

			unsigned long ip = inet_addr("127.0.0.1"); printf("%ld\n", ip);

运转后果:
16777343


图解 sockaddr_in 构造体


为什么要搞这么复杂,构造体中嵌套构造体,而不必 sockaddr_in 的一个成员变量来指明IP地址呢?socket() 函数的第一个参数曾经指清楚明了地址类型,为什么在 sockaddr_in 构造体中还要再阐明一次呢,这不是烦琐吗?
这些繁琐的细节的确给初学者带来了必定的妨碍,我想,这或许是汗青缘由吧,前面的接口总要兼容后面的代码。列位读者必定要有耐烦,临时不睬解没有关系,依据教程中的代码“照猫画虎”即可,工夫久了天然会承受。

为什么运用 sockaddr_in 而不运用 sockaddr

bind() 第二个参数的类型为 sockaddr,而代码中却运用 sockaddr_in,然后再强迫转换为 sockaddr,这是为什么呢?
sockaddr 构造体的界说如下:

			struct sockaddr{ sa_family_t sin_family; //地址族(Address Family),也就是地址类型 char sa_data[14]; //IP地址和端标语 };

下图是 sockaddr 与 sockaddr_in 的比照(括号中的数字表现所占用的字节数):

sockaddr 和 sockaddr_in 的长度相反,多是16字节,只是将IP地址和端标语兼并到一同,用一个成员 sa_data 表现。要想给 sa_data 赋值,必需同时指明IP地址和端标语,例如”127.0.0.1:80“,遗憾的是,没有相干函数将这个字符串转换成需求的方式,也就很难给 sockaddr 类型的变量赋值,所以运用 sockaddr_in 来替代。这两个构造体的长度相反,强迫转换类型时不会丧失字节,也没有过剩的字节。
可以以为,sockaddr 是一种通用的构造体,可以用来保管多品种型的IP地址和端标语,而 sockaddr_in 是专门用来保管 IPv4 地址的构造体。别的还有 sockaddr_in6,用来保管 IPv6 地址,它的界说如下:

			struct sockaddr_in6 { sa_family_t sin6_family; //(2)地址类型,取值为AF_INET6 in_port_t sin6_port; //(2)16位端标语 uint32_t sin6_flowinfo; //(4)IPv6流信息 struct in6_addr sin6_addr; //(4)详细的IPv6地址 uint32_t sin6_scope_id; //(4)接口规模ID };

恰是因为通用构造体 sockaddr 运用方便,才针对分歧的地址类型界说了分歧的构造体。

connect() 函数

connect() 函数用来树立衔接,它的原型为:

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);  //Linux
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);  //Windows

各个参数的阐明和 bind() 相反,不再赘述。