对应书籍

《tcp/ip网络编程》
第二章 套接字类型与协议设置
第三章 地址族与数据序列

socket函数的三个参数

我们上节调用的创建套接字的函数int sock(int domain, int type, int protocal);, 这一章讨论下这后面的三个参数.

1. domain-协议族(Protocol Family)

一般选择IPv4协议族,其余的用的比较少.
tcp/ip网络编程[笔记二] - 图1

2. type-套接字类型(Type)

主要分成以下两种

套接字类型1: 面向连接的套接字(SOCK_STREAM)

  • 特点:

    1. 传输过程中数据不会丢失(可靠传输)
    2. 按序传输数据(保证顺序性)
    3. 传输的数据不存在数据边界(面向字节流的传输)

套接字类型2: 面向消息的套接字(SOCK_DGRAM)

  • 特点:

    1. 强调快速传输而非传输顺序
    2. 传输的数据可能丢失也可能损毁
    3. 传输的数据有数据边界
    4. 限制每次传输的大小

3. protocal-协议的最终选择

对于IPv4协议族,前两个参数选好了, 最后一个参数也就确定了, 因为就两种协议TCP和UDP. 这里就不细讲这两个协议啦.

第二章总结

这二章节讲的很少, 就讲述了两种传输方式, 还写了一个代码验证了socket缓冲区的存在. 当然课后题还是得看看:

  1. 什么是协议?在收发数据中定义协议有何意义.
    协议就是“计算机对话必备通信规则”

sockaddr_in结构体详解

我们在第一章编写的服务器代码和客户端代码都用到了这个结构体, 此结构体就是存储套接字的信息.
先来看看具体定义:

  1. /*
  2. * Socket address, internet style.
  3. */
  4. struct sockaddr_in {
  5. sa_family_t sin_family; //地址族(Address Family)
  6. in_port_t sin_port; //16位TCP/UDP端口号
  7. struct in_addr sin_addr; //32位IP地址
  8. char sin_zero[8]; //不使用
  9. };

成员fin_family:

tcp/ip网络编程[笔记二] - 图2

成员sin_addr

存储IP地址, 以32位无符号整形存储, 采用网络字节序(后面会介绍什么是网络字节序).

  1. /*
  2. * Internet address (a structure for historical reasons)
  3. */
  4. struct in_addr {
  5. in_addr_t s_addr;
  6. };
  7. typedef __uint32_t in_addr_t; /* base type for internet address */

成员sin_zero

无特殊含义, 全部填充成0, 为了保持结构体sockaddr_in和结构体sockaddr长度一致.
我们在调用bind时用到以下代码:

  1. if (bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr))==-1){
  2. error_handling((char*)"bind() error");
  3. }

我们会发现bind函数的第二个参数实际上是sockaddr,sockaddr长这样:

  1. /*
  2. * [XSI] Structure used by kernel to store most addresses.
  3. */
  4. struct sockaddr {
  5. sa_family_t sa_family; /* [XSI] address family */
  6. char sa_data[14]; /* [XSI] addr value (actually larger) */
  7. };

不难发现, sockaddr_in中的sin_port占2个字节, sin_addr占4个字节, sin_zero占8个字节, tcp/ip网络编程[笔记二] - 图3, 刚好和sockaddr的sa_data长度一样. 现在明白sockaddr_in为啥后面多了一个无实际意义的sin_zero了吧.
那么有的人可能会问sockaddr为啥要把sa_data长度设置成14个字节呢, 这个是为了以后的扩展性

字节序

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

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

大家看两幅图就明白了:
tcp/ip网络编程[笔记二] - 图4
tcp/ip网络编程[笔记二] - 图5

网络字节序和字节序转换

网络字节序就是大端序.
字节序转换函数介绍:

  1. unsigned short htons(unsigned short);
  2. unsigned short ntohs(unsigned short);
  3. unsigned long htonl(unsigned long);
  4. unsigned long ntohl(unsigned long);
  5. s指short(两字节), l指long(四字节), h代表主机(host)字节序, n代表网络(network)字节序

实例代码

网络地址初始化与分配

我们已经详细学习了结构体sockaddr_in, 之后我们来学习初始化和分配的一般流程, 这个在第一章的代码中其实有用过. 我这里只是单独拿出来, 方便记忆.

  • 初始化代码
    1. struct sockaddr_in my_addr;
    2. char * serv_ip = "121.37.12.39";
    3. char * serv_port = "9190"
    4. memset(&my_addr, 0, sizeof(my_addr));
    5. my_addr.sin_family = AF_INET;
    6. my_addr.sin_addr.s_addr = inet_addr(serv_ip);//注意这一行
    7. my_addr.sin_port = htons(atoi(serv_port));


上述代码中,有几个地方需要注意:

  • inet_addr函数

    • 函数原型:
      1. #include<arpa/inet.h>
      2. in_addr_t inet_addr(const char *string);
  1. -

作用: 把点分十进制表示法(Dotted Decimal Notation)转换成网络字节序的整形.

  • INADDR_ANY
    对于服务器, 当只有一张网卡时, 其实IP地址只有一个, 这时候就可以用这个函数,
    把上面代码中第六行改成my_addr.sin_addr.s_addr = htonl(INADDR_ANY).
  • 分配代码
    1. //serv_sock是套接字文件描述符, serv_addr是sockaddr_in结构体, 我们假设他们已经事先创建好了
    2. bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));


只有服务器端才会使用bind, 客户端直接connect, 这是因为我们只用知道服务器端的IP地址和端口号.

第三章总结

这一章在讨论如何向套接字分配地址, 重点讲了结构体sockaddr_in, 讲的还是比较简单. 题目没有什么需要思考的, 都是记忆性知识.