在之后的文章中我们将来讲解网络编程中的相关知识点,再本文中我们首先来讲解一下网络编程中的预备知识:
预备知识
源IP地址和目的IP地址
在IP数据包中有两个IP地址分别是源IP地址和目的IP地址,此时这里就会出现一个问题就是:如果我们光有IP地址,是无法完成通信的。有了IP地址只能够将消息发送到对方的机器上,但是还徐亚欧有一个其他的标识位来进行区分,这个数据需要发送给哪一个程序进行解析。
端口号
端口号是(port)是传输层协议的内容
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用。
需要注意的是:一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定
理解端口号和进程ID
之前我们在学习系统编程的时候,学习过在Linux中pid可以表示一个进程;此处在网络编程中端口号也表示唯一的一个进程,那么这两者有什么关系?对于此处的理解在我看来就是:我们使用的操作系统有很多,那么就意味着每一个操作系统都有着自己的进程ID的处理方式,但是网络在目前只有一个,所有的操作系统都需要接入这个网络,有了端口号就可以让网络在各个操作系统上都可以正常的运行,不用让网络来对操作系统进行适配,同时可以让网络与操作系统进行解耦。
认识TCP协议和UDP协议
在此处我们简单直观的了解一下TCP协议和UDP协议,后面我们再对其进行详细的学习。
TCP协议
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP协议
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
网络字节序
在之前学习C语言的时候,我们遇到过一个问题就是:计算机内存中的多字节序对于内存地址有大段和小段之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大段小段之分。对于网络数据流来说同样有着大段小段之分。
- 发送主机通常将发送缓冲区的数据按照内存地址从低到高的顺序发出;
- 接受主机把网络上接收到的字节依次保存在接受缓冲区中,也是按照内存地址从低到高的顺序保存;
- 因此网络数据流的地址应该这样规定:先发出的数据是低地址,后发出的数据是高地址;
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节;
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端,就需要将数据转成大端;否则就忽略,直接发送即可。
例子:将0x1234abcd写入到以0x0000开始的内存中,则结果为:
big-endian | little-endian | |
---|---|---|
0x0000 | 0x12 | 0xcd |
0x0001 | 0x34 | 0xab |
0x0002 | 0xab | 0x34 |
0x0003 | 0xcd | 0x12 |
为了使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用一下的库函数做网络字节序和主机字节序的转换。 |
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
// 这几个函数名都比较好记: h表示host, n表示network, l表示32为长整数, s表示16为短整数
// htonl 表示将32位的长整数从主机字节序转换成网络字节序,例如将IP地址转化后准备发送
// 如果主机是小段字节序,这些函数将参数做相应的大小端转换然后返回
// 如果主机是大段字节序,这些函数将不做转换,将参数原封不动的返回
socket 常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
在上述的API中我们都可以看到一个类型 – struct sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6。然而, 各种网络协议的地址格式并不相同。
struct sockaddr和struct sockaddr_in这两个结构体用来处理网络通信的地址。
- 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结构体指针做为参数;
sockaddr结构
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的结构是sockaddr_in,这个结构体中主要有三部分信息:地址类型,端口号,IP地址
sockaddr_in
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
// -----------------------------------------------------------------
// __SOCKADDR_COMMON (sin_);的定义如下:
/*
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
sa_prefix##family 是一个预处理宏中的宏替换语法。它的作用是将两个标识符合并成一个单词,并用于创建结构体的字段名。在这个语法中:
sa_prefix 是一个标识符(通常是一个结构体字段的前缀),它是宏的一个参数。
## 是预处理器中的连接运算符,用于将两个标识符连接在一起,形成一个新的标识符。
family 是另一个标识符(通常是表示套接字地址族的字段名称),它在这里与 sa_prefix 合并。
// Type to represent a port.
typedef uint16_t in_port_t;
// Internet address.
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
*/
后面的文章中我们将开始使用UDP协议、TCP协议等来进行代码的编写。