1. 预备知识
1.1 源IP地址和目的IP地址
源IP地址:指的就是发送数据包的那个电脑的IP地址。
目的IP地址:就是想要发送到的那个电脑的IP地址。
1.2 认识端口号
端口号(port)是传输层协议的内容。
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用。
1.3 理解 “端口号” 和 “进程ID”
系统编程的pid 表示唯一一个进程;此处我们的端口号也是唯一表示一个进程。
一个进程可以绑定多个端口号;
一个端口号不能被多个进程绑定。
1.4 源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号,就是在描述 “数据是谁发的,要发给谁”。
1.5 认识UDP协议
我们先对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;后面再详细讨论。
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
1.6 认识TCP协议
此处我们也是对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;后面再详细讨论。
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
1.7 网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分。
那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址;
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节;
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
为使网络程序具有可移植性, 使同样的C代码在大端和小端计算机上编译后都能正常运行, 可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // 主机字节序转网络字节序,转化4字节
uint16_t htons(uint16_t hostshort); // 主机字节序转网络字节序,转化2字节
uint32_t ntohl(uint32_t netlong); // 网络字节序转主机字节序,转化4字节
uint16_t ntohs(uint16_t netshort); // 网络字节序转主机字节序,转化2字节
- h表示host, n表示network, l表示32位长整数, s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序, 例如将IP地址转换后准备发送。
- 如果主机是小端字节序, 这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序, 这些函数不做转换, 将参数原封不动地返回。
2. socket编程接口
2.1 socket 常见API
2.1.1 创建
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
参数:
domain:地址域 —> 网络层使用什么协议
AF_INET: ipv4版本的ip协议
AF_INET6: ipv6版本的ip协议
AF_UNIX: 域套接字
type:套接字的类型
SOCK_DGRAM: 用户数据报套接字 —> 默认协议是UDP协议
SOCK_STREAM: 流式套接字 —> 默认的协议是TCP协议
protocol:协议
SOCK_DGRAM: IPPROTO_UDP(17)
SOCK_STREAM: IPPROTO_TCP(6)
也可以传递0,表示使用套接字的默认协议
返回值:套接字描述符,本质上还是一个文件描述符
- socket()打开一个网络通讯端口, 如果成功的话, 就像open()一样返回一个文件描述符;
- 应用程序可以像读写文件一样用read/write在网络上收发数据;
- 如果socket()调用出错则返回-1;
- 对于IPv4, family参数指定为AF_INET;
- 对于TCP协议, type参数指定为SOCK_STREAM, 表示面向流的传输协议;
- protocol参数的介绍从略, 指定为0即可。
2.1.2 绑定
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addr_len);
参数:
sockfd:套接字描述符
addr:地址信息
addr_len:传入结构体的真实字节数量
- 服务器程序所监听的网络地址和端口号通常是固定不变的, 客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;
- 服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()成功返回0, 失败返回-1;
- bind()的作用是将参数sockfd和addr绑定在一起;
- struct sockaddr *是一个通用指针类型, addr参数实际上可以接受多种协议的sockaddr结构体, 而它们的长度各不相同, 所以需要参数addr_len指定结构体的长度。
注意绑定ip的时候:
- 可以直接绑定网卡对应的ip地址;
- 也可以绑定0.0.0.0表示当前机器当中的任意网卡的ip地址;
- 绑定127.0.0.1表示绑定本地回环地址,只访问本地网络协议栈(访问自己,测试地址)。
2.1.3 关闭
// 关闭套接字
close(int sockfd);
参数:
sockfd:套接字描述符
2.2 sockaddr结构
socket API是一层抽象的网络编程接口, 适用于各种底层网络协议, 如IPv4、IPv6。然而, 各种网络协议的地址格式并不相同。
2.2.1 sockaddr 结构
//通用的结构体:
struct sockaddr
{
sa_family_t sa_family; //地址类型, AF_xxx, 2字节
char sa_data[14]; // 协议地址, 14字节
};
2.2.2 sockaddr_in 结构
// AF_INET的结构体
struct sockaddr_in
{
sa_family_t sin_family; //地址类型, 2字节
in_port_t sin_port; // 端口号, 2字节
struct in_addr sin_addr; // IP地址, 4字节
unsigned char sin_zero[8]; // 8字节填充, 目的是与addr对齐
};
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in。
这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。
2.2.3 in_addr 结构
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
in_addr用来表示一个IPv4的IP地址,其实就是一个32位的整数。
2.2.4 小结
-
IPv4和IPv6的地址格式定义在netinet/in.h中, IPv4地址用sockaddr_in结构体表示, 包括16位地址类型, 16位端口号和32位IP地址。
-
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样, 只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体, 就可以根据地址类型字段确定结构体中的内容。
-
socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
2.3 地址转换函数
基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换。
// 点分十进制转网络二进制
#include <netinet/in.h>
in_addr_t inet_addr(const char *cp);
inet addr()函数的作用是:将Internet主机地址从IPv4数字和点符号转换为按网络字节顺序的二进制数据。如果输入无效,则返回INADDR_NONE(通常为-1)。
2.4 例子
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h> // 套接字
#include <netinet/in.h> // IP地址——> 网络字节序
#include <arpa/inet.h> // 主机 ——> 网络字节序
#include <iostream>
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(sockfd < 0)
{
perror(