1.端口号介绍
端口号是传输层协议的内容,端口号的类型是2字节的16位的整数,端口号是用来标识一个进程,使操作系统知道当前这个数据要交给哪一个进程处理。
IP地址+端口号能够标识网络上的某一台主机的某一个进程,一个端口号只能被一个进程占用。
2.端口号范围划分
0-1023:知名端口号,HTTP,ETP,SSH等这些广为使用的应用层协议,它们的端口号都是规定的。
1024-65535:操作系统动态分配的端口号,客户系统程序的端口号由操作系统在这个范围随机分配。
3.端口号和进程ID区别
pid是可以表示唯一一个进程,而现在端口号也可以表示唯一一个进程,区别是一个进程可以绑定多个端口号,但是端口号不能被多个进程绑定,因为端口号不知道在多分进程如何找到指定的进程。
进程ID属于系统概念,技术上也是唯一性,确实可以用来标识唯一的一个进程,但是做了的话,就会让系统进程管理和网络强耦合。
4.源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号,简单理解就是" 谁发给谁 ",这两个端口号就是表示两个谁是谁。
补充如何通过端口号找到对应进程:
但接收方的传输层接收到数据后,需要判断这个数据报给哪一个进程处理,而端口号是在网络协议中使用的标识,不能与应用层进程关联。所以,操作系统会维护一个端口号和进程pid的映射表(哈希结构),拿到端口号就可以直接执行哈希函数找到对应的进程,进而完成通信。
5.socket
IP地址就是用来标识互联网中唯一的一台主机,port(端口号)用来标识唯一的一个网络进程。
IP+port就能表示互联网中唯一的一个进程,所以通信时,本质就是两个互联网进程进行通信(srcip,srcport,dstip,dstport)这样的四元组就能标识互联网中的唯二的两个进程。
网络通信的本质就是进程间通信,ip+port叫做套接字socket。
6.TCP协议介绍
TCP是一种面向连接的,可靠的,基于字节流的传输层通信协议,简单来说,它就像是一个非常负责任的快递员,要确保货物(数据)能够完整无误地从发送方送到接收方。
特点:
1.面向连接:在数据传输之前,TCP需要建立一个连接,就像打电话一样,先拨号,对方接听后,才能开始交流。
2.可靠传输:TCP会保证数据的可靠传输,对发送的数据进行编号,接收方会收到数据后发送消息给发送方,如果发送方在一定时间内没有接收到确认信息,就会重新发送数据。
3.流量控制:TCP采用滑动窗口机制控制变量,如水管的水流速度不能太快,负责就会溢出来,发送方会根据接收方的接收能力来调整发送数据的速度,接收方的缓冲区满了,就会通知发送方减慢发送速度,避免接收方因为数据太多而处理不过来。
4.拥塞控制:网络出现拥塞(交通阻塞一样,数据传输速度变慢)时,TCP会调整自己的传输速度,通过检测丢包情况来判断网络是否拥塞,丢包率高就会认为网络拥塞,就会减少发送数据的速度,避免进一步加重网络负担。
7.UDP协议介绍
UDP是一种无连接的,简单的面向数据的传输层协议,就像是一个不太负责的快递员,只是把包裹扔出去,不关心包裹是否能完整到达目的地。
特点:
1.无连接:UDP发送数据之前不需要建立连接,就像发送短信一样,直接把消息发送出去就行。发送方只需要知道接收方的地址和端口号,就可以发送数据报。
2.不可靠传输:UDP不保证数据能可靠传输,不会对数据进行编号和确认,也不会重新发送数据。
3.简单高效:UDP没有TCP那么复杂的机制,协议开销小,传输速度快。
8.网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端和小端之分,网络数据流向同样有大端和小端之分,就需要一个规定。
发送主机通常将发送的缓冲区的数据按内存地址从低到高顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也就是按内存地址从低到高的顺序保存;
TCP/IP协议规定,网络数据采用大端字节序,即低地址高字节。
如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送。
网络字节序和主机字节序转行函数
1. **`htonl(uint32_t hostlong)`**
- **功能**:将一个32位的主机字节序整数转换为网络字节序整数。
- **参数**:`hostlong`,一个32位的整数,以主机字节序表示。
- **返回值**:转换后的32位整数,以网络字节序表示。
- **用途**:当你需要将一个整数从主机字节序转换为网络字节序(大端字节序)时使用,特别是在发送数据到网络之前。2. **`htons(uint16_t hostshort)`**
- **功能**:将一个16位的主机字节序整数转换为网络字节序整数。
- **参数**:`hostshort`,一个16位的整数,以主机字节序表示。
- **返回值**:转换后的16位整数,以网络字节序表示。
- **用途**:当你需要将一个短整型(short)从主机字节序转换为网络字节序时使用,这在处理网络协议中的16位字段时很常见。3. **`ntohl(uint32_t netlong)`**
- **功能**:将一个32位的网络字节序整数转换为主机字节序整数。
- **参数**:`netlong`,一个32位的整数,以网络字节序表示。
- **返回值**:转换后的32位整数,以主机字节序表示。
- **用途**:当你从网络接收到数据并需要将其转换为主机字节序时使用,这样可以在本地系统中正确处理这些数据。4. **`ntohs(uint16_t netshort)`**
- **功能**:将一个16位的网络字节序整数转换为主机字节序整数。
- **参数**:`netshort`,一个16位的整数,以网络字节序表示。
- **返回值**:转换后的16位整数,以主机字节序表示。
- **用途**:当你从网络接收到16位的数据(如端口号、某些协议字段等)并需要将其转换为主机字节序时使用。这些函数在网络编程中非常重要,因为不同的计算机系统可能使用不同的字节序,而网络协议通常规定使用大端字节序。通过这些函数,开发者可以确保数据在不同系统间的一致性和正确性。
9.socket编程接口
创建套接字函数
int socket(int domain, int type, int protocol);
-
参数:
-
domain
:指定协议族。常用的有AF_INET
(IPv4)和AF_INET6
(IPv6)。 -
type
:指定套接字类型。常用的有SOCK_STREAM
(TCP)和SOCK_DGRAM
(UDP)。 -
protocol
:指定协议。对于TCP,通常使用IPPROTO_TCP
;对于UDP,使用IPPROTO_UDP
。如果设置为0,系统会自动选择合适的协议。
-
-
返回值:
-
成功时返回一个非负整数,即套接字描述符。
-
失败时返回
-1
,并设置errno
以指示错误原因。
-
绑定端口号函数
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
-
参数:
-
socket
:由socket
函数返回的套接字描述符。 -
address
:指向sockaddr
结构的指针,该结构包含了套接字的地址信息,如IP地址和端口号。 -
address_len
:sockaddr
结构的长度。
-
-
返回值:
-
成功时返回0。
-
失败时返回
-1
,并设置errno
以指示错误原因。
-
sockaddr结构
套接字编程的种类:1.域间套接字(本地的进程间通信) 2.原始套接字 3.网络套接字编程
套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信,原始套接字主要是用来编写网络工具。网络套接字编程主要利用传输层来实现进程间通信。
套接字提供了sockaddr_in和sockaddr_un结构体
sockaddr_in结构体是用于跨网络通信
sockaddr_un结构体是用于本地通信
为了让套接字的网络通信和本地通信可以使用同一套函数接口,就有了sockaddr结构体,这三个结构体的头部16个比特位是一样的,这个字段也叫协议家族。
根据前16个比特位判断是网络通信还是本地通信。跨网络通信需要提供IP地址,端口号等,本地通信只需要一个路径名。
注意:在进行网络通信时,需要定义sockaddr_in和sockaddr_un这样的结构体,只不过在传参时需要将该结构体的地址类型进行强转成sockaddr*类型。
之所以参数不用 void*是当时不支持这个语法,后面没改是为了保证现有的代码不出问题,改动可能引发连锁问题。
sockaddr结构
sockaddr_in结构
in_addr结构
关于_SOCKADDR_COMMON宏
`__SOCKADDR_COMMON(sin_);` 这个宏的调用实际上是在定义 `struct sockaddr_in` 结构体时传入参数的。这里的参数 `sin_` 是一个前缀,用于生成结构体中的成员名称。
让我们详细解释一下这个过程:
### 宏定义
首先,宏 `__SOCKADDR_COMMON` 是这样定义的:
```c
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
```这个宏接受一个参数 `sa_prefix`,并将其与 `##`(连接运算符)一起使用,生成一个新的标识符。具体来说,它会生成 `sa_prefix##family`,即将 `sa_prefix` 和 `family` 连接在一起。
### 使用宏
在定义 `struct sockaddr_in` 结构体时,我们使用这个宏:
```c
struct sockaddr_in {
__SOCKADDR_COMMON(sin_); /* Port number */
in_port_t sin_port; /* Internet address */
struct in_addr sin_addr; /* Internet address */
};
```在这里,我们传入的参数是 `sin_`。宏展开后,生成的代码如下:
```c
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
in_port_t sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
};
```### 解释
- `sa_family_t sin_family;`:这是生成的成员,表示地址族(Address Family)。在这个例子中,`sin_` 是传入的前缀,`family` 是宏中定义的后缀,连接在一起生成 `sin_family`。
- `in_port_t sin_port;`:这是端口号,用于指定网络端口。
- `struct in_addr sin_addr;`:这是 IPv4 地址,用于指定网络地址。### 为什么使用宏
使用宏的好处是可以简化代码,使其更加通用和可维护。例如,如果我们有多个类似的结构体(如 `struct sockaddr_in` 和 `struct sockaddr_in6`),我们可以使用同一个宏来定义它们的通用部分,从而避免重复代码。
### 示例:定义 `struct sockaddr_in6`
```c
struct sockaddr_in6 {
__SOCKADDR_COMMON(sin6_); /* Transport layer port number */
in_port_t sin6_port; /* Transport layer port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Set of interfaces for a scope */
};
```在这里,我们传入的参数是 `sin6_`。宏展开后,生成的代码如下:
```c
struct sockaddr_in6 {
sa_family_t sin6_family; /* Address family */
in_port_t sin6_port; /* Transport layer port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Set of interfaces for a scope */
};
```通过这种方式,我们可以确保不同结构体中的通用部分保持一致,同时减少代码重复。