@Author:清秋橘 Autumn_Tangerine
@Date:2021/7/18
@Last Modified:2021/7.18
序言
最近跟着学校老师在做一个上线的项目,时间紧任务重,需要尽量在这个暑假前就把大概的东西都写好,因为主要涉及到了将VB/VC代码迁移到QT for Linux上,其中大部分为Socket网络编程操作,之前接触到的一部分网络编程知识渐渐有点不够用弄了,所以通过各种渠道把C的网络编程重新学了一遍
发现网上关于这部分的博客其实并不是很多而且质量都不高,所以打算自己写一篇,主要供自己以后复习用
如果很幸运的这篇博客可以被其他人读到,那就一起努力吧
在阅读此博客之前,为了确保你可以顺利读懂该博客,你需要具备以下方面的知识
- 对计算机网络的基本了解,至少需要对TCP/IP协议及应用层协议有基本的认识
- C/C++编程基础
- Linux/Windows系统操作(非必要)
关于计算机网络这部分的相关知识,可以看我另一篇博客《花七天时间,从流程上总结贯通整个计算机网络知识》
本博客主要基于IPV4和Linux平台的Socket编写,其他部分仅有少量涉及,仅供参考
理解Socket
在计算机网络基础课中学到过,Socket是IP地址+端口号,比如{192.168.1.1 :: 8888},但是我觉得这个描述并不是十分的恰当
Socket又称为套接字,插座,运行在计算机中的两个程序通过Socket建立起一个通道,数据在通道中进行传输,它把复杂的TCP/IP协议族隐藏了起来,对于C++网络编程来说,Socket是其自带的一个库,这个库里面封装了传输数据或者接受数据时用到的各种操作,对于用于而言,进行数据传输便不用对底层TCP/IP直接进行复杂的操作,而只需要用Socket相关的函数,就可以完成网络通信
相关的库函数
基于Linux平台的Socket库
socket
int socket(int domain, int type, int protocol);
socket函数用于创建一个新的socket,也就是向系统申请一个socket资源,或者这里就可以按照计算机网络基础课中那样理解,socket函数就是像系统申请到了一个IP地址+端口号
参数说明:
- domain:协议域,又称协议族(family)。常用的协议族有AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等,这里经常用到的是前两种
- type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。
- protocol:指定协议,常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
返回值:
成功则返回一个socket,失败返回-1,错误原因存于errno 中,除非系统资料耗尽,socket函数一般不会返回失败
一般来说,创建Socket第一个参数填AF_INET,第二个参数填SOCK_STREAM,第三个参数填0
这里第三个参数填的意思是IPPROTO_IP,即接收任何的IP数据包。其中的校验和和协议分析由程序自己完成,而如果你选择了比如IPPROTO_TCP,那么,他就只会接受TCP数据包
gethostbyname
struct hostent *gethostbyname(const char *name);
把ip地址或域名转换为hostent 结构体表达的地址
参数说明:
name,域名或者主机名,例如”192.168.1.3″、”www.autumn-tangerine.cn“等
返回值:
如果成功,返回一个hostent结构指针,失败返回NULL。
inet_addr
in_addr_t inet_addr(const char *cp);
参数说明:
传入参数为”a.b.c.d”格式时,inet_addr()会检查abcd每个字段是否>255,如果有>255的字段,则返回INADDR_NODE
但当传入参数为”a.b.d”、“a.d”、”d”时,inet_addr()会把除d外,其余字段从前往后填入点分十进制对应字段,d则用来补完剩下的字段
另外,inet_addr()支持字段使用8进制和16进制

换句话说,这个函数主要的作用就是将字符串格式的IP地址转换整数类型地址
返回值:
如果正确执行将返回一个无符号长整数型数。如果传入的字符串不是一个合法的IP地址,将返回INADDR_NONE。

inet_pton()
int inet_pton(int af, const char *src, void *dst);
参数说明:
- af:地址簇
- src:来源地址
- dst:用于接收转换后的数据
inet_pton()对于传入的参数只支持”a,b,c,d”格式,同时不支持8进制及16进制输入
这个函数转换字符串到网络地址,是inet_addr的扩展
af = AF_INET时,src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中。
af = AF_INET6时,src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中。
返回值:
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0
inet_addr()相比于inet_pton()更为宽松,但inet_pton支持IPV6,inet_addr()有时不符合我们对地址格式的要求,这时可以使用inet_pton()对传入的格式进行有效的判断
int check_ip(const char *ip)
{
struct in_addr p;
int ret = inet_pton(AF_INET, ip, &p);
if (ret == 0 || errno == EAFNOSUPPORT) {
return -1;
}
return 0;
}
主机字节序和网络字节序
首先,我们应该先搞清楚字节顺序是一个什么概念
字节顺序是指战内存多于一个字节类型的数据在内存中的存放顺序,比如一个32位整数由4个字节组成,内存中存储这4个字节有两种办法,一种是将底序字节存储在起始位置,这成为小端字节序,另一种方法是将高序字节存储在起始位置,这成为大端字节序
而在不同的主机当中,由于CPU的设计不同,就算是在同一操作系统中,甚至是在同一机器上的两个进程(C语言与Java采用不同的字节序),数据存储的字节顺序仍然有可能不同,而如果要求数据能够在不同的两台主机之间通信的话,就需要沟通好一致的字节顺序,所以,在网络字节序中,为了保证数据在不同主机之间传输时能够被正确解释,网络字节序统一规定了字节顺序为大端字节序
所以,在进行通信之前,不管主机采用什么字节顺序,都需要将主机字节序先转换为网络字节序,其过程中涉及到的转换函数:
htons,htonl = host to network short/long
htonl()函数的格式为
u_long htonl(u_long hostlong);
其中,参数hostlong是主机字节序的数据,该函数的返回值是转换之后的网络字节序的数据
htons()函数的格式为,z
u_short htons(u_short hostshort);
该函数的用法与htonl()类似,htonl用于32位无符号数的转换,htons用于16位无符号数的转换
常用结构体
sockaddr
struct sockaddr{
unsigned short sa_family;//地址族,AF_xxx AF_INET 不涉及转序的问题
char sa_data[14]; //14字节的协议地址(端口+地址),网络字节序
};
sa_family是地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET sa_data是14字节协议地址
此结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息。但因为该结构中端口号与地址在同一个变量sa_data中存储,不便于操作,所欲实际编程中并不直接针对此数据结构操作,而是使用另一个与sockaddr等价的数据结构,sockaddr_in
sockaddr_in
struct sockaddr_in { short int sin_family; //地址族 unsigned short int sin_port; //端口号 struct in_addr sin_addr; //Internet地址 unsigned char sin_zero[8]; //为了保持struct sockaddr 一样的长度,16个字节 };
sin_port存储端口号(使用网络字节顺序) sin_addr存储IP地址,使用in_addr这个数据结构 sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
此时:
struct in_addr{ union{ struct{u_char s_b1,s_b2,s_b3,s_b4;}S_un_b; struct{u_short s_w1,s_w2;}S_un_w; u_long S_addr;//网络字节顺序 }S_un; };
这里涉及到union(联合)的概念,它是一种特殊的类。通过关键字union进行定义,一个union可以有多个数据成员,在任意时刻,联合中只能有一个数据成员可以有值。当给联合中某个成员赋值之后,该联合中的其它成员就变成未定义状态了
s_addr按照网络字节顺序存储IP地址
S_un_b和S_un_w分别为把IP地址且切分为四个char字符存入和将IP地址切分为两个无符号short类型存入,因为这俩种方式不是很常用,所以也可以理解为:
struct in_addr{ u_long S_addr;//网络字节顺序 };
所以同时也有:
struct sockaddr_in { short int sin_family; //地址族 unsigned short int sin_port; //端口号 u_long S_addr; //Internet地址 unsigned char sin_zero[8]; //为了保持struct sockaddr 一样的长度,16个字节 };
当然,这样写只是便于理解,实际上sockaddr_in中并没有S_addr这个变量
sockaddr与sockaddr_in二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr
sockaddr_in6
struct sockaddr_in6{ __SOCKADDR_COMMON (sin6_); in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id; }
IPV6结构体,不做过多介绍
hostent
struct hostent { char *h_name; //正式主机名 char **h_aliases; //主机别名 int h_addrtype; //主机IP地址类型:IPV4-AF_INET int h_length; //主机IP地址字节长度,对于IPv4是四字节,即32位 char **h_addr_list; //主机的IP地址列表 };
- h_name,它是一个字符指针,指向主机名
- h_aliases是一个二重指针,指向主机的别名,一个主机可以有多个别名,h_aliases可以用下标的方式访问不同的主机别名
- h_addrtype,为int型数据,指定主机使用的IP地址类型
- h_length指定IP地址长度
- h_addr_list也是一个二重指针,与h_aliases一样,也可以通过下标进行访问。它指定主机的不同IP地址,在实际的应用中,一台服务器往往有好几个IP地址,而域名只有一个,这样设计的好处是,可以使系统分布设计,提升服务器的稳定性和抗灾难能力。一般对服务器的访问,则是先经过DNS服务器,DNS通过均衡设计,返回合适的IP与客户端进行交互,避免客户端只连接一个IP,导致网络拥堵
从网上搜寻了很多资料,包括CSDN,关于这部分的讲解很乱,无非就是你抄我我抄你,根本没有点实质性的东西,所以这里的东西加入了一点自己的理解
个人认为,hostent可以理解为存储一个主机信息的结构体,比如对于百度来说
正式主机名为www.a.shifen.com,www.baidu.com为别名,
参考资料
本博客在编写时参考到了以下网络或书籍上的资源,感谢
- 开发者知识库《创建socket函数的第三个参数的意义》
- CSDN 《主机字节序与网络序》
- CSDN 《socket常用结构体详解》
- CSDN 《inet_addr和inet_pton的使用》
- CSDN 《hostent实例详解》