socket网络字节序以及大端序小端序

不同CPU中,4字节整数1在内存空间的存储方式是不同的。4字节整数1可用2进制表示如下:

00000000 00000000 00000000 00000001

有些CPU以上面的顺序存储到内存,另外一些CPU则以倒序存储,如下所示:

00000001 00000000 00000000 00000000

若不考虑这些就收发数据会发生问题,因为保存顺序的不同意味着对接收数据的解析顺序也不同。

大端序和小端序

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

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


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


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


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


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


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

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

网络字节序转换函数

在《使用bind()和connect()函数》一节中讲解了 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. int main(){
  6. unsigned short host_port = 0x1234, net_port;
  7. unsigned long host_addr = 0x12345678, net_addr;
  8. net_port = htons(host_port);
  9. net_addr = htonl(host_addr);
  10. printf("Host ordered port: %#x\n", host_port);
  11. printf("Network ordered port: %#x\n", net_port);
  12. printf("Host ordered address: %#lx\n", host_addr);
  13. printf("Network ordered address: %#lx\n", net_addr);
  14. system("pause");
  15. return 0;
  16. }

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

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

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



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

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

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

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

在socket中使用域名

客户端中直接使用IP地址会有很大的弊端,一旦IP地址变化(IP地址会经常变动),客户端软件就会出现错误。

而使用域名会方便很多,注册后的域名只要每年续费就永远属于自己的,更换IP地址时修改域名解析即可,不会影响软件的正常使用。

关于域名注册、域名解析、host 文件、DNS 服务器等本节并未详细讲解,请读者自行脑补。本节重点讲解如何使用域名。

通过域名获取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 结构体变量的组成如下图所示:



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



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

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

理解UDP套接字

TCP 是面向连接的传输协议,建立连接时要经过三次握手,断开连接时要经过四次握手,中间传输数据时也要回复ACK包确认,多种机制保证了数据能够正确到达,不会丢失或出错。

UDP 是非连接的传输协议,没有建立连接和断开连接的过程,它只是简单地把数据丢到网络中,也不需要ACK包确认。

UDP 传输数据就好像我们邮寄包裹,邮寄前需要填好寄件人和收件人地址,之后送到快递公司即可,但包裹是否正确送达、是否损坏我们无法得知,也无法保证。UDP 协议也是如此,它只管把数据包发送到网络,然后就不管了,如果数据丢失或损坏,发送端是无法知道的,当然也不会重发。

既然如此,TCP应该是更加优质的传输协议吧?

如果只考虑可靠性,TCP的确比UDP好。但UDP在结构上比TCP更加简洁,不会发送ACK的应答消息,也不会给数据包分配Seq序号,所以UDP的传输效率有时会比TCP高出很多,编程中实现UDP也比TCP简单。

UDP 的可靠性虽然比不上TCP,但也不会像想象中那么频繁地发生数据损毁,在更加重视传输效率而非可靠性的情况下,UDP是一种很好的选择。比如视频通信或音频通信,就非常适合采用UDP协议;通信时数据必须高效传输才不会产生“卡顿”现象,用户体验才更加流畅,如果丢失几个数据包,视频画面可能会出现“雪花”,音频可能会夹带一些杂音,这些都是无妨的。

与UDP相比,TCP的生命在于流控制,这保证了数据传输的正确性。

最后需要说明的是:TCP的速度无法超越UDP,但在收发某些类型的数据时有可能接近UDP。例如,每次交换的数据量越大,TCP 的传输速率就越接近于 UDP。

基于UDP的服务器端和客户端

前面的文章中我们给出了几个TCP的例子,对于UDP而言,只要能理解前面的内容,实现并非难事。

UDP中的服务器端和客户端没有连接

UDP不像TCP,无需在连接状态下交换数据,因此基于UDP的服务器端和客户端也无需经过连接过程。也就是说,不必调用 listen() 和 accept() 函数。UDP中只有创建套接字的过程和数据交换的过程。

UDP服务器端和客户端均只需1个套接字

TCP中,套接字是一对一的关系。如要向10个客户端提供服务,那么除了负责监听的套接字外,还需要创建10套接字。但在UDP中,不管是服务器端还是客户端都只需要1个套接字。之前解释UDP原理的时候举了邮寄包裹的例子,负责邮寄包裹的快递公司可以比喻为UDP套接字,只要有1个快递公司,就可以通过它向任意地址邮寄包裹。同样,只需1个UDP套接字就可以向任意主机传送数据。

基于UDP的接收和发送函数

创建好TCP套接字后,传输数据时无需再添加地址信息,因为TCP套接字将保持与对方套接字的连接。换言之,TCP套接字知道目标地址信息。但UDP套接字不会保持连接状态,每次传输数据都要添加目标地址信息,这相当于在邮寄包裹前填写收件人地址。

发送数据使用 sendto() 函数:



  1. ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen); //Linux
  2. int sendto(SOCKET sock, const char *buf, int nbytes, int flags, const struct sockadr *to, int addrlen); //Windows

Linux和Windows下的 sendto() 函数类似,下面是详细参数说明:

  • sock:用于传输UDP数据的套接字;
  • buf:保存待传输数据的缓冲区地址;
  • nbytes:带传输数据的长度(以字节计);
  • flags:可选项参数,若没有可传递0;
  • to:存有目标地址信息的 sockaddr 结构体变量的地址;
  • addrlen:传递给参数 to 的地址值结构体变量的长度。


UDP 发送函数 sendto() 与TCP发送函数 write()/send() 的最大区别在于,sendto() 函数需要向他传递目标地址信息。

接收数据使用 recvfrom() 函数:



  1. ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen); //Linux
  2. int recvfrom(SOCKET sock, char *buf, int nbytes, int flags, const struct sockaddr *from, int *addrlen); //Windows

由于UDP数据的发送端不不定,所以 recvfrom() 函数定义为可接收发送端信息的形式,具体参数如下:

  • sock:用于接收UDP数据的套接字;
  • buf:保存接收数据的缓冲区地址;
  • nbytes:可接收的最大字节数(不能超过buf缓冲区的大小);
  • flags:可选项参数,若没有可传递0;
  • from:存有发送端地址信息的sockaddr结构体变量的地址;
  • addrlen:保存参数 from 的结构体变量长度的变量地址值。

基于UDP的回声服务器端/客户端

下面结合之前的内容实现回声客户端。需要注意的是,UDP不同于TCP,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务器端和客户端,只是因为其提供服务而称为服务器端,希望各位读者不要误解。

下面给出Windows下的代码,Linux与此类似,不再赘述。

服务器端 server.cpp:



  1. #include <stdio.h>
  2. #include <winsock2.h>
  3. #pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
  4. #define BUF_SIZE 100
  5. int main(){
  6. WSADATA wsaData;
  7. WSAStartup( MAKEWORD(2, 2), &wsaData);
  8. //创建套接字
  9. SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
  10. //绑定套接字
  11. sockaddr_in servAddr;
  12. memset(&servAddr, 0, sizeof(servAddr)); //每个字节都用0填充
  13. servAddr.sin_family = PF_INET; //使用IPv4地址
  14. servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取IP地址
  15. servAddr.sin_port = htons(1234); //端口
  16. bind(sock, (SOCKADDR*)&servAddr, sizeof(SOCKADDR));
  17. //接收客户端请求
  18. SOCKADDR clntAddr; //客户端地址信息
  19. int nSize = sizeof(SOCKADDR);
  20. char buffer[BUF_SIZE]; //缓冲区
  21. while(1){
  22. int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &clntAddr, &nSize);
  23. sendto(sock, buffer, strLen, 0, &clntAddr, nSize);
  24. }
  25. closesocket(sock);
  26. WSACleanup();
  27. return 0;
  28. }

代码说明:
1) 第12行代码在创建套接字时,向 socket() 第二个参数传递 SOCK_DGRAM,以指明使用UDP协议。

2) 第18行代码中使用htonl(INADDR_ANY)来自动获取IP地址。

利用常数 INADDR_ANY 自动获取IP地址有一个明显的好处,就是当软件安装到其他服务器或者服务器IP地址改变时,不用再更改源码重新编译,也不用在启动软件时手动输入。而且,如果一台计算机中已分配多个IP地址(例如路由器),那么只要端口号一致,就可以从不同的IP地址接收数据。所以,服务器中优先考虑使用INADDR_ANY;而客户端中除非带有一部分服务器功能,否则不会采用。

客户端 client.cpp:



  1. #include <stdio.h>
  2. #include <WinSock2.h>
  3. #pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
  4. #define BUF_SIZE 100
  5. int main(){
  6. //初始化DLL
  7. WSADATA wsaData;
  8. WSAStartup(MAKEWORD(2, 2), &wsaData);
  9. //创建套接字
  10. SOCKET sock = socket(PF_INET, SOCK_DGRAM, 0);
  11. //服务器地址信息
  12. sockaddr_in servAddr;
  13. memset(&servAddr, 0, sizeof(servAddr)); //每个字节都用0填充
  14. servAddr.sin_family = PF_INET;
  15. servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
  16. servAddr.sin_port = htons(1234);
  17. //不断获取用户输入并发送给服务器,然后接受服务器数据
  18. sockaddr fromAddr;
  19. int addrLen = sizeof(fromAddr);
  20. while(1){
  21. char buffer[BUF_SIZE] = {0};
  22. printf("Input a string: ");
  23. gets(buffer);
  24. sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&servAddr, sizeof(servAddr));
  25. int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &fromAddr, &addrLen);
  26. buffer[strLen] = 0;
  27. printf("Message form server: %s\n", buffer);
  28. }
  29. closesocket(sock);
  30. WSACleanup();
  31. return 0;
  32. }

先运行 server,再运行 client,client 输出结果为:

Input a string: C语言中文网
Message form server: C语言中文网
Input a string: c.biancheng.net Founded in 2012
Message form server: c.biancheng.net Founded in 2012
Input a string:


从代码中可以看出,server.cpp 中没有使用 listen() 函数,client.cpp 中也没有使用 connect() 函数,因为 UDP 不需要连接。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C++实习生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值