【转载】socket编程常用函数解析

这里以一个socket通信例子讲解socket的函数,server.cpp 是服务器端代码,client.cpp 是客户端代码,要实现的功能是:客户端从服务器读取一个字符串并打印出来。

服务器端代码 server.cpp:

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <arpa/inet.h>
  6. #include <sys/socket.h>
  7. #include <netinet/in.h>
  8.  
  9. int main(){
  10. //创建套接字
  11. int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  12.  
  13. //将套接字和IP、端口绑定
  14. struct sockaddr_in serv_addr;
  15. memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
  16. serv_addr.sin_family = AF_INET; //使用IPv4地址
  17. serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
  18. serv_addr.sin_port = htons(1234); //端口
  19. bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
  20.  
  21. //进入监听状态,等待用户发起请求
  22. listen(serv_sock, 20);
  23.  
  24. //接收客户端请求
  25. struct sockaddr_in clnt_addr;
  26. socklen_t clnt_addr_size = sizeof(clnt_addr);
  27. int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
  28.  
  29. //向客户端发送数据
  30. char str[] = "http://c.biancheng.net/socket/";
  31. write(clnt_sock, str, sizeof(str));
  32.  
  33. //关闭套接字
  34. close(clnt_sock);
  35. close(serv_sock);
  36.  
  37. return 0;
  38. }


客户端代码 client.cpp:

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <arpa/inet.h>
  6. #include <sys/socket.h>
  7.  
  8. int main(){
  9. //创建套接字
  10. int sock = socket(AF_INET, SOCK_STREAM, 0);
  11.  
  12. //向服务器(特定的IP和端口)发起请求
  13. struct sockaddr_in serv_addr;
  14. memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
  15. serv_addr.sin_family = AF_INET; //使用IPv4地址
  16. serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
  17. serv_addr.sin_port = htons(1234); //端口
  18. connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
  19.  
  20. //读取服务器传回的数据
  21. char buffer[40];
  22. read(sock, buffer, sizeof(buffer)-1);
  23.  
  24. printf("Message form server: %s\n", buffer);
  25.  
  26. //关闭套接字
  27. close(sock);
  28.  
  29. return 0;
  30. }


启动一个终端(Shell),先编译 server.cpp 并运行:

[admin@localhost ~]$ g++ server.cpp -o server
[admin@localhost ~]$ ./server
#等待请求的到来

正常情况下,程序运行到 accept() 函数就会被阻塞,等待客户端发起请求。

接下再启动一个终端,编译 client.cpp 并运行:

[admin@localhost ~]$ g++ client.cpp -o client
[admin@localhost ~]$ ./client
Message form server: http://c.biancheng.net/socket/

client 接收到从 server发送过来的字符串就运行结束了,同时,server 完成发送字符串的任务也运行结束了。大家可以通过两个打开的终端来观察。

client 运行后,通过 connect() 函数向 server 发起请求,处于监听状态的 server 被激活,执行 accept() 函数,接受客户端的请求,然后执行 write() 函数向 client 传回数据。client 接收到传回的数据后,connect() 就运行结束了,然后使用 read() 将数据读取出来。

server 只接受一次 client 请求,当 server 向 client 传回数据后,程序就运行结束了。如果想再次接收到服务器的数据,必须再次运行 server,所以这是一个非常简陋的 socket 程序,不能够一直接受客户端的请求。

源码解析

1) 先说一下 server.cpp 中的代码。

第 11 行通过 socket() 函数创建了一个套接字,参数 AF_INET 表示使用 IPv4 地址,SOCK_STREAM 表示使用面向连接的套接字,IPPROTO_TCP 表示使用 TCP 协议。在 Linux 中,socket 也是一种文件,有文件描述符,可以使用 write() / read() 函数进行 I/O 操作。

第 19 行通过 bind() 函数将套接字 serv_sock 与特定的 IP 地址和端口绑定,IP 地址和端口都保存在 sockaddr_in 结构体中。

socket() 函数确定了套接字的各种属性,bind() 函数让套接字与特定的IP地址和端口对应起来,这样客户端才能连接到该套接字。

第 22 行让套接字处于被动监听状态。所谓被动监听,是指套接字一直处于“睡眠”中,直到客户端发起请求才会被“唤醒”。

第 27 行的 accept() 函数用来接收客户端的请求。程序一旦执行到 accept() 就会被阻塞(暂停运行),直到客户端发起请求。

第 31 行的 write() 函数用来向套接字文件中写入数据,也就是向客户端发送数据。

和普通文件一样,socket 在使用完毕后也要用 close() 关闭。

2) 再说一下 client.cpp 中的代码。client.cpp 中的代码和 server.cpp 中有一些区别。

第 19 行代码通过 connect() 向服务器发起请求,服务器的IP地址和端口号保存在 sockaddr_in 结构体中。直到服务器传回数据后,connect() 才运行结束。

第 23 行代码通过 read() 从套接字文件中读取数据。

 

Linux 下的 socket() 函数

在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:

int socket(int af, int type, int protocol);

1) af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

大家需要记住127.0.0.1,它是一个特殊IP地址,表示本机地址,后面的教程会经常用到。

2) type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。
3) protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

有了地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?

正如大家所想,一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

本教程使用 IPv4 地址,参数 af 的值为 PF_INET。如果使用 SOCK_STREAM 传输数据,那么满足这两个条件的协议只有 TCP,因此可以这样来调用 socket() 函数:

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议

这种套接字称为 TCP 套接字。

如果使用 SOCK_DGRAM 传输方式,那么满足这两个条件的协议只有 UDP,因此可以这样来调用 socket() 函数:

int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP协议

这种套接字称为 UDP 套接字。

上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议,如下所示:

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字

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 绑定:

  1. //创建套接字
  2. int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  3.  
  4. //创建sockaddr_in结构体变量
  5. struct sockaddr_in serv_addr;
  6. memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
  7. serv_addr.sin_family = AF_INET; //使用IPv4地址
  8. serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
  9. serv_addr.sin_port = htons(1234); //端口
  10.  
  11. //将套接字和IP、端口绑定
  12. bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

这里我们使用 sockaddr_in 结构体,然后再强制转换为 sockaddr 类型,后边会讲解为什么这样做。

sockaddr_in 结构体

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

  1. struct sockaddr_in{
  2. sa_family_t sin_family; //地址族(Address Family),也就是地址类型
  3. uint16_t sin_port; //16位的端口号
  4. struct in_addr sin_addr; //32位IP地址
  5. char sin_zero[8]; //不使用,一般用0填充
  6. };

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 类型的结构体,该结构体只包含一个成员,如下所示:

  1. struct in_addr{
  2. in_addr_t s_addr; //32位的IP地址
  3. };

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

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

运行结果:
16777343


图解 sockaddr_in 结构体


为什么要搞这么复杂,结构体中嵌套结构体,而不用 sockaddr_in 的一个成员变量来指明IP地址呢?socket() 函数的第一个参数已经指明了地址类型,为什么在 sockaddr_in 结构体中还要再说明一次呢,这不是啰嗦吗?

这些繁琐的细节确实给初学者带来了一定的障碍,我想,这或许是历史原因吧,后面的接口总要兼容前面的代码。各位读者一定要有耐心,暂时不理解没有关系,根据教程中的代码“照猫画虎”即可,时间久了自然会接受。

为什么使用 sockaddr_in 而不使用 sockaddr

bind() 第二个参数的类型为 sockaddr,而代码中却使用 sockaddr_in,然后再强制转换为 sockaddr,这是为什么呢?

sockaddr 结构体的定义如下:

  1. struct sockaddr{
  2. sa_family_t sin_family; //地址族(Address Family),也就是地址类型
  3. char sa_data[14]; //IP地址和端口号
  4. };

下图是 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 地址,它的定义如下:

  1. struct sockaddr_in6 {
  2. sa_family_t sin6_family; //(2)地址类型,取值为AF_INET6
  3. in_port_t sin6_port; //(2)16位端口号
  4. uint32_t sin6_flowinfo; //(4)IPv6流信息
  5. struct in6_addr sin6_addr; //(4)具体的IPv6地址
  6. uint32_t sin6_scope_id; //(4)接口范围ID
  7. };

正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。

对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。

listen() 函数

通过 listen() 函数可以让套接字进入被动监听状态,它的原型为:

  1. int listen(int sock, int backlog); //Linux
  2. int listen(SOCKET sock, int backlog); //Windows

sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。

所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。

请求队列

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。

如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。

当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误,对于 Windows,客户端会收到 WSAECONNREFUSED 错误。

注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。

accept() 函数

当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:

  1. int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); //Linux
  2. SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen); //Windows

它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。

accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

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() 相同,不再赘述,这里说下accept和connect函数区别

accept函数用来接收客户的请求,程序一旦执行到accept函数就会阻塞,直到客户端发起请求

connect函数表示客户端向服务端发起请求

 

数据的接收和发送函数

网络连接也是一个文件,他也有文件描述符,只要用socket()创建了连接,剩下的就是文件操作,Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。

前面我们说过,两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。

write() 的原型为:

ssize_t write(int fd, const void *buf, size_t nbytes);

fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。

size_t 是通过 typedef 声明的 unsigned int 类型;ssize_t 在 "size_t" 前面加了一个"s",代表 signed,即 ssize_t 是通过 typedef 声明的 signed int 类型。

write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。

read() 的原型为:

ssize_t read(int fd, void *buf, size_t nbytes);

fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。

read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

关闭socket函数

调用 close()/closesocket() 函数意味着完全断开连接,即不能发送数据也不能接收数据,这种“生硬”的方式有时候会显得不太“优雅”。

close()/closesocket() 断开连接
图1:close()/closesocket() 断开连接


上图演示了两台正在进行双向通信的主机。主机A发送完数据后,单方面调用 close()/closesocket() 断开连接,之后主机A、B都不能再接受对方传输的数据。实际上,是完全无法调用与数据收发有关的函数。

一般情况下这不会有问题,但有些特殊时刻,需要只断开一条数据传输通道,而保留另一条。

使用 shutdown() 函数可以达到这个目的,它的原型为:

  1. int shutdown(int sock, int howto); //Linux
  2. int shutdown(SOCKET s, int howto); //Windows

sock 为需要断开的套接字,howto 为断开方式。

howto 在 Linux 下有以下取值:

  • SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
  • SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
  • SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。


howto 在 Windows 下有以下取值:

  • SD_RECEIVE:关闭接收操作,也就是断开输入流。
  • SD_SEND:关闭发送操作,也就是断开输出流。
  • SD_BOTH:同时关闭接收和发送操作。


至于什么时候需要调用 shutdown() 函数,下节我们会以文件传输为例进行讲解。

close()/closesocket()和shutdown()的区别

确切地说,close() / closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。

shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。

调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。

默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。

大端序和小端序

CPU 向内存保存数据的方式有两种:

  • 大端序(Big Endian):高位字节存放到低位地址(高位字节在前)。
  • 小端序(Little Endian):高位字节存放到高位地址(低位字节在前)。


仅凭描述很难解释清楚,不妨来看一个实例。假设在 0x20 号开始的地址中保存 4 字节 int 型数据 0x12345678,大端序 CPU 保存方式如下图所示:

整数 0x12345678 的大端序字节表示
图1:整数 0x12345678 的大端序字节表示


对于大端序,最高位字节 0x12 存放到低位地址,最低位字节 0x78 存放到高位地址。小端序的保存方式如下图所示:

整数 0x12345678 的小端序字节表示
图2:整数 0x12345678 的小端序字节表示


不同 CPU 保存和解析数据的方式不同(主流的 Intel 系列 CPU 为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。

主机 A 先把数据转换成大端序再进行网络传输,主机 B 收到数据后先转换为自己的格式再解析。

网络字节序转换函数

  网络字节序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节序采用big endian排序方式。

主机字节序

  不同的机器主机字节序不相同,与CPU设计有关,数据的顺序是由cpu决定的,而与操作系统无关。我们把某个给定系统所用的字节序称为主机字节序(host byte order)。比如x86系列CPU都是little-endian的字节序。

  由于这个原因不同体系结构的机器之间无法通信,所以要转换成一种约定的数序,也就是网络字节顺序。

 

网络字节序与主机字节序之间的转换函数:htons(), ntohs(), htons(),htonl(),位于头文件<netinet/in.h>,htons和ntohs完成16位无符号数的相互转换,htonl和ntohl完成32位无符号数的相互转换。

在前面几篇文章中讲解了 sockaddr_in 结构体,其中就用到了网络字节序转换函数,如下所示:

  1. //创建sockaddr_in结构体变量
  2. struct sockaddr_in serv_addr;
  3. memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
  4. serv_addr.sin_family = AF_INET; //使用IPv4地址
  5. serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
  6. serv_addr.sin_port = htons(1234); //端口号

htons() 用来将当前主机字节序转换为网络字节序,其中h代表主机(host)字节序,n代表网络(network)字节序,s代表short,htons 是 h、to、n、s 的组合,可以理解为”将 short 型数据从当前主机字节序转换为网络字节序“。

常见的网络字节转换函数有:

  • htons():host to network short,将 short 类型数据从主机字节序转换为网络字节序。
  • ntohs():network to host short,将 short 类型数据从网络字节序转换为主机字节序。
  • htonl():host to network long,将 long 类型数据从主机字节序转换为网络字节序。
  • ntohl():network to host long,将 long 类型数据从网络字节序转换为主机字节序。


通常,以s为后缀的函数中,s代表 2 个字节 short,因此用于端口号转换;以l为后缀的函数中,l代表 4 个字节的 long,因此用于 IP 地址转换。

举例说明上述函数的调用过程:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <WinSock2.h>
  4. #pragma comment(lib, "ws2_32.lib")
  5.  
  6. int main(){
  7. unsigned short host_port = 0x1234, net_port;
  8. unsigned long host_addr = 0x12345678, net_addr;
  9.  
  10. net_port = htons(host_port);
  11. net_addr = htonl(host_addr);
  12.  
  13. printf("Host ordered port: %#x\n", host_port);
  14. printf("Network ordered port: %#x\n", net_port);
  15. printf("Host ordered address: %#lx\n", host_addr);
  16. printf("Network ordered address: %#lx\n", net_addr);
  17.  
  18. system("pause");
  19. return 0;
  20. }

运行结果:
Host ordered port: 0x1234
Network ordered port: 0x3412
Host ordered address: 0x12345678
Network ordered address: 0x78563412

ip地址转换函数:inet_ntoa() 和 inet_addr()

另外需要说明的是,sockaddr_in 中保存 IP 地址的成员为 32 位整数,而我们熟悉的是点分十进制表示法,例如 127.0.0.1,它是一个字符串,因此为了分配 IP 地址,需要将字符串转换为 4 字节整数。

inet_ntoa():  将网络字节序的整型值转化为字符串形式的IP地址

函数原型:
char *inet_ntoa(struct in_addr);
参数:in_addr是一个结构体,用来表示一个32位的IPV4地址。
  struct in_addr{
           in_addr_t s_addr;
       }

返回值:返回点分十进制的字符串在静态内存中的指针。

点分十进制:

全称为点分(点式)十进制表示法,是IPV4的IP地址标识方法。
IPV4中用4个字节表示一个IP地址,每个字节按照十进制表示为0~255。
点分十进制就是用4个从0~255的数字,来表示一个IP地址。
例如:192.168.1.246
头文件:<arpa/inet.h>

别称:IP地址转换函数。

功能:将网络字节序IP转化成点分十进制IP

网络字节序:网络字节序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian(大端)排序方式。

inet_addr():将字符串形式的IP地址转为网络字节顺序的整型值

简介: 
inet_addr方法可以转化字符串,主要用来将一个十进制的数转化为二进制的数,用途多余IPV4的IP转化。
函数原型:
 in_addr_t inet_addr(const char* cp);
1
参数:字符串,一个点分十进制的IP地址。
返回值: 
若字符串有效,则将字符串转换为32位二进制网络字节序的IPV4地址;否则,为INADDR_NONE
头文件:<arpa/inet.h>
别称:IP地址转化函数。
功能:将一个点分十进制的IP转换成一个长整数型(u_long类型)。
 

inet_addr() 除了将字符串转换为 32 位整数,同时还进行网络字节序转换。请看下面的代码:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <WinSock2.h>
  4. #pragma comment(lib, "ws2_32.lib")
  5.  
  6. int main(){
  7. char *addr1 = "1.2.3.4";
  8. char *addr2 = "1.2.3.256";
  9.  
  10. unsigned long conv_addr = inet_addr(addr1);
  11. if(conv_addr == INADDR_NONE){
  12. puts("Error occured!");
  13. }else{
  14. printf("Network ordered integer addr: %#lx\n", conv_addr);
  15. }
  16.  
  17. conv_addr = inet_addr(addr2);
  18. if(conv_addr == INADDR_NONE){
  19. puts("Error occured!");
  20. }else{
  21. printf("Network ordered integer addr: %#lx\n", conv_addr);
  22. }
  23.  
  24. system("pause");
  25. return 0;
  26. }

运行结果:
Network ordered integer addr: 0x4030201
Error occured!

从运行结果可以看出,inet_addr() 不仅可以把 IP 地址转换为 32 位整数,还可以检测无效 IP 地址。

注意:为 sockaddr_in 成员赋值时需要显式地将主机字节序转换为网络字节序,而通过 write()/send() 发送数据时 TCP 协议会自动转换为网络字节序,不需要再调用相应的函数。

通过域名获取IP地址

域名仅仅是 IP 地址的一个助记符,目的是方便记忆,通过域名并不能找到目标计算机,通信之前必须要将域名转换成 IP 地址。

gethostbyname() 函数可以完成这种转换,它的原型为:

  1. struct hostent *gethostbyname(const char *hostname);

hostname 为主机名,也就是域名。使用该函数时,只要传递域名字符串,就会返回域名对应的 IP 地址。返回的地址信息会装入 hostent 结构体,该结构体的定义如下:

  1. struct hostent{
  2. char *h_name; //official name
  3. char **h_aliases; //alias list
  4. int h_addrtype; //host address type
  5. int h_length; //address lenght
  6. char **h_addr_list; //address list
  7. }

从该结构体可以看出,不只返回 IP 地址,还会附带其他信息,各位读者只需关注最后一个成员 h_addr_list。下面是对各成员的说明:

  • h_name:官方域名(Official domain name)。官方域名代表某一主页,但实际上一些著名公司的域名并未用官方域名注册。
  • h_aliases:别名,可以通过多个域名访问同一主机。同一 IP 地址可以绑定多个域名,因此除了当前域名还可以指定其他域名。
  • h_addrtype:gethostbyname() 不仅支持 IPv4,还支持 IPv6,可以通过此成员获取IP地址的地址族(地址类型)信息,IPv4 对应 AF_INET,IPv6 对应 AF_INET6。
  • h_length:保存IP地址长度。IPv4 的长度为 4 个字节,IPv6 的长度为 16 个字节。
  • h_addr_list:这是最重要的成员。通过该成员以整数形式保存域名对应的 IP 地址。对于用户较多的服务器,可能会分配多个 IP 地址给同一域名,利用多个服务器进行均衡负载。


hostent 结构体变量的组成如下图所示:

hostent 结构体的组成


下面的代码主要演示 gethostbyname() 的应用,并说明 hostent 结构体的特性:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <WinSock2.h>
  4. #pragma comment(lib, "ws2_32.lib")
  5.  
  6. int main(){
  7. WSADATA wsaData;
  8. WSAStartup( MAKEWORD(2, 2), &wsaData);
  9.  
  10. struct hostent *host = gethostbyname("www.baidu.com");
  11. if(!host){
  12. puts("Get IP address error!");
  13. system("pause");
  14. exit(0);
  15. }
  16.  
  17. //别名
  18. for(int i=0; host->h_aliases[i]; i++){
  19. printf("Aliases %d: %s\n", i+1, host->h_aliases[i]);
  20. }
  21.  
  22. //地址类型
  23. printf("Address type: %s\n", (host->h_addrtype==AF_INET) ? "AF_INET": "AF_INET6");
  24.  
  25. //IP地址
  26. for(int i=0; host->h_addr_list[i]; i++){
  27. printf("IP addr %d: %s\n", i+1, inet_ntoa( *(struct in_addr*)host->h_addr_list[i] ) );
  28. }
  29.  
  30. system("pause");
  31. return 0;
  32. }

运行结果:
Aliases 1: www.baidu.com
Address type: AF_INET
IP addr 1: 61.135.169.121
IP addr 2: 61.135.169.125

如何让服务器端持续不断地监听客户端的请求?

不管服务器端还是客户端,都有一个问题,就是处理完一个请求立即退出了,没有太大的实际意义。能不能像Web服务器那样一直接受客户端的请求呢?能,使用 while 循环即可。

修改回声程序,使服务器端可以不断响应客户端的请求。

服务器端 server.cpp:

  1. #include <stdio.h>
  2. #include <winsock2.h>
  3. #pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
  4.  
  5. #define BUF_SIZE 100
  6.  
  7. int main(){
  8. WSADATA wsaData;
  9. WSAStartup( MAKEWORD(2, 2), &wsaData);
  10.  
  11. //创建套接字
  12. SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
  13.  
  14. //绑定套接字
  15. sockaddr_in sockAddr;
  16. memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
  17. sockAddr.sin_family = PF_INET; //使用IPv4地址
  18. sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
  19. sockAddr.sin_port = htons(1234); //端口
  20. bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
  21.  
  22. //进入监听状态
  23. listen(servSock, 20);
  24.  
  25. //接收客户端请求
  26. SOCKADDR clntAddr;
  27. int nSize = sizeof(SOCKADDR);
  28. char buffer[BUF_SIZE] = {0}; //缓冲区
  29. while(1){
  30. SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
  31. int strLen = recv(clntSock, buffer, BUF_SIZE, 0); //接收客户端发来的数据
  32. send(clntSock, buffer, strLen, 0); //将数据原样返回
  33.  
  34. closesocket(clntSock); //关闭套接字
  35. memset(buffer, 0, BUF_SIZE); //重置缓冲区
  36. }
  37.  
  38. //关闭套接字
  39. closesocket(servSock);
  40.  
  41. //终止 DLL 的使用
  42. WSACleanup();
  43.  
  44. return 0;
  45. }


客户端 client.cpp:

  1. #include <stdio.h>
  2. #include <WinSock2.h>
  3. #include <windows.h>
  4. #pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
  5.  
  6. #define BUF_SIZE 100
  7.  
  8. int main(){
  9. //初始化DLL
  10. WSADATA wsaData;
  11. WSAStartup(MAKEWORD(2, 2), &wsaData);
  12.  
  13. //向服务器发起请求
  14. sockaddr_in sockAddr;
  15. memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
  16. sockAddr.sin_family = PF_INET;
  17. sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
  18. sockAddr.sin_port = htons(1234);
  19.  
  20. char bufSend[BUF_SIZE] = {0};
  21. char bufRecv[BUF_SIZE] = {0};
  22.  
  23. while(1){
  24. //创建套接字
  25. SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  26. connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
  27. //获取用户输入的字符串并发送给服务器
  28. printf("Input a string: ");
  29. gets(bufSend);
  30. send(sock, bufSend, strlen(bufSend), 0);
  31. //接收服务器传回的数据
  32. recv(sock, bufRecv, BUF_SIZE, 0);
  33. //输出接收到的数据
  34. printf("Message form server: %s\n", bufRecv);
  35.  
  36. memset(bufSend, 0, BUF_SIZE); //重置缓冲区
  37. memset(bufRecv, 0, BUF_SIZE); //重置缓冲区
  38. closesocket(sock); //关闭套接字
  39. }
  40.  
  41. WSACleanup(); //终止使用 DLL
  42. return 0;
  43. }

while(1) 让代码进入死循环,除非用户关闭程序,否则服务器端会一直监听客户端的请求。客户端也是一样,会不断向服务器发起连接。

需要注意的是:server.cpp 中调用 closesocket() 不仅会关闭服务器端的 socket,还会通知客户端连接已断开,客户端也会清理 socket 相关资源,所以 client.cpp 中需要将 socket() 放在 while 循环内部,因为每次请求完毕都会清理 socket,下次发起请求时需要重新创建。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值