1、字节序(大端模式、小端模式)

字节(Byte)

  • 传输和存储信息的最小单位。
  • 各种数据类型,都是由字节构成。

主机字节序(Big-Endian | Little-Endian)

  • 不同类型的机器处理数据时,会按特定的字节排列顺序读取存储器。

以unsigned short类型的数据0x0002为例

大端模式:

  • 字节0x00放在低位地址标识的字节
  • 字节0x02放在高位地址标识的字节

0000000000000010

低 → 高

小端模式:

  • 字节0x00放在高位地址标识的字节
  • 字节0x02放在低位地址标识的字节

0000001000000000

低 → 高

2、主机字节序、网络字节序,转换函数

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

网络字节顺序采用Big-Endian排序方式,总是从低位地址开始传输。发送数据包时,程序将主机字节序转换为网络字节序;接受收数据包时,则将网络字节序转换为主机字节序。
htons、ntohs、htonl、ntohl

主机字节序变为网络字节序

  • uint16_t htons(uint16_t hostshort)
  • uint32_t htonl(uint32_t hostlong)

网络字节序变为主机字节序

  • uint16_t ntohs(uint16_t netshort)
  • uint32_t ntohl(uint16_t netlong)

3、数据结构对齐,对齐系数

数据(字长)模型是编译器用来确定基本数据类型长度的,有 LP64、ILP64、LLP64、ILP32、LP32。

integers (I), long integers (L), and pointers (P)

  • Linux/Unix: LP64
  • Windows: LLP64

对齐系数

  • 每个特定平台的编译器都有自己的默认“对齐系数” 。
  • 通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是指定的“对齐系数”。

结构体对齐规则:

①结构体变量的首地址能够被其最宽基本类型成员的大小所整除

②结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;

③结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

4、套接字

主动套接字、被动套接字

  • 被动:等待传入连接的套接字,如服务器套接字
  • 主动:发起连接的套接字,如客户端套接字

端到端地址

创建时不指定,使用时指明(TCP/IP需要指明协议端口号和IP地址)

  • TCP/IP协议族:PF_INET

  • TCP/IP地址族:AF_INET

  • PF是protocol family,AF是address family,TCP/IP套接字设计者认为可能某个协议族有多种形式的地址,所以在API上把它们分开了,创建socket用PF,bind/connect用AF。但如今一个PF只有一个AF,从来没有过例外,所以可混用

套接字类型

  • SOCKET_DGRAM: 双向不可靠数据报,对应UDP
  • SOCKET_STREAM:双向可靠数据流,对应TCP
  • SOCKET_RAW:低于传输层的低级协议或物理网络提供的套接字类型,可以访问内部网络接口。

5、地址转换函数

  • Socket 通用地址结构sockaddr

    • Socket是传输层/网络层编程接口,由于传输层/网络层的各种实现不同,可能会有不同的编址方案。通用地址为了适应这种需求而定义。
    • 通用地址有很大局限性,实际并不具有通用性,例如针对AF_INET6/AF_LOCAL类型的Socket地址,sockaddr结构实际上只能标识出这个地址的类型。
  • 人们习惯使用 202.112.14.151 表示地址(点分十进制),但是这个本质是一个字符串而不是数值,因此在socket编程时,需要进行转换。此外还要考虑字节序的问题,为此可以使用如下一些函数:

    1. int inet_aton(const char *cp, struct in_addr *inp)

      返回:1-串有效,0-串有错

      inet_aton函数将cp所指的字符串转换成32位的网络字节序二进制,并通过指针inp来存储。这个函数需要对字符串所指的地址进行有效性验证。但如果cp为空,函数仍然成功,但不存储任何结果。

    2. in_addr_t inet_addr(const char *cp)

      返回:若成功,返回32位二进制的网络字节序地址,若有错,则返回INADDR_NONE

      inet_addr进行相同的转换,但不进行有效性验证,也就是说,所有232种可能的二进制值对inet_addr函数都是有效的——过时函数,不应再使用

    3. char *inet_ntoa(struct in_addr in)

      返回:指向点分十进制数串的指针

      • 函数inet_ntoa将32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串。但由于返回值所指向的串留在静态内存中,这意味着函数是不可重入的。
      • 需要注意的是这个函数是以结构为参数,而不是指针。

      上述三个地址转换函数都只能处理IPv4协议,而不能处理IPv6地址。在同时要处理IPv4和v6的程序中,建议使用以下两个函数。

    4. int inet_pton(int family, const char *src, void *dst)

      返回:1-成功,0-输入无效,-1:出错

      将src指向的字符串转换成二进制地址数值放到dst中。

    5. const char *inet_ntop(int family, const void *src, char *dst, size_t cnt)

      返回:指向结果的指针--成功,NULL-出错

      和pton做相反的操作。

      • family参数可以是AF_INET,也可以是AF_INET6。
      • 如果长度参数cnt太小,无法容纳表达式格式结果,则返回一个空串。另外,目标指针dst调用前必须先由调用者分配空间。

6、简单TCP循环服务器Socket编程基本步骤(服务器端和客户端)

服务器端

  1. 创建套接字
  2. 绑定套接字
  3. 设置套接字为监听模式,进入被动接受连接状态
  4. 接受请求,建立连接
  5. 读写数据
  6. 终止连接

客户端

  1. 创建套接字
  2. 与远程服务器建立连接
  3. 读写数据
  4. 终止连接

7、简单UDP循环服务器Socket编程基本步骤(服务器端和客户端)

服务器端

  1. 建立UDP套接字;
  2. 绑定套接字到特定地址;
  3. 等待并接收客户端信息;
  4. 处理客户端请求;
  5. 发送信息回客户端;
  6. 关闭套接字;

客户端

  1. 建立UDP套接字;
  2. 发送信息给服务器;
  3. 接收来自服务器的信息;
  4. 关闭套接字