文章目录
- 正确理解端口号
- 理解源IP地址和目的IP地址
- 认识端口号
- 端口号和进程ID
 
- 理解Socket
- 网络字节序
- socket编程接口
- 创建socket套接字
- bind绑定套接字
- listen建立监听
- accept接受连接
- connect建立连接
- sendto发送数据
- 接收数据
- close关闭套接字
 
- sockaddr结构体
 
正确理解端口号
理解源IP地址和目的IP地址
我们知道通过IP地址我们能在网络中唯一标识一台主机,这也就意味着,只要我知道你的IP地址,就能向你的主机发送数据。但是这有一个问题,主机接收到数据之后发送给哪个进程呢?也就是说,光靠IP地址我们只能找到目标主机,但是更为详细的选择哪个进程就无能为力了。所以我们需要有端口号。端口号就是主机唯一标识进程的一个16字节整数,拿着IP地址和端口号,我们就能在浩瀚的网络中找到目标主机的目标进程。更为准确的,网络通信本质还是进程间通信,只不过这两个进程可能不在一台主机上。
 
认识端口号
根据上面介绍,总结端口号的特点:
- 端口号是一个2字节16位的整数
- 端口号用来标识一个进程,告诉操作系统,当前从网卡收到的数据要交给哪一个进程
- IP地址+端口号能在网络世界中标识唯一一个进程
- 同一台主机下,一个端口号只能被一个进程占用
更为具体地:端口号地范围是0-65535,其中
- 0-1023表示的是知名端口号,一般是系统或者是特权进程使用
- 1024-65535:操作系统动态分配的端口号,客户端程序的端口号就是由操作系统从这个范围分配的
端口号和进程ID
我们说到端口号是为了唯一标识一台主机的某个进程,但是为什么不直接使用进程PID呢?进程PID的作用也是标识唯一一个进程呐。原因如下:
- PID标识的是正在运行中的进程,一旦程序退出,这个进程就找不到了
- PID是变化的,可能每次运行程序得到的PID都不一样
- 更深层面上来说,进程ID属于操作系统层面,端口号处于网络层面,如果端口号和进程PID绑定,会让系统进程管理和网络强耦合,实际设计的时候,并没有选择这么做。
理解Socket
综上所述,网络中的两个进程要想相互通信,通信的数据得明确四个属性:源主机的IP+Port,目标主机的IP+Port。我们把IP+Port的组合就称为套接字Socket。
网络字节序
我们知道,数据在内存中的存储方式有大小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大小端之分,网络数据流也有大小端之分。如果传输数据双方的主机一个是大端存储,一个是小端存储,假设我们不做任何处理就将数据发送能够给对方,那对方拿到的数据就会出现乱码。为了避免这种情况,我们需要在将数据发送到网络之前将数据转换成网络字节序,具体的:
 TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节,所以如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
此外,我们可以在代码中调用相关库函数来做网络字节序和主机字节序的转换:
 
 其中
- htonl函数的作用是将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送
- htons函数的作用是将 16 位的短整数从主机字节序转换为网络字节序,例如将 端口号 地址转换后准备发送
- ntohl函数的作用是将 32 位的长整数从网络字节序转换为主机字节序,例如将 接收到的 IP 地址转换后使用
- ntohs函数的作用是将 16 位的短整数从网络字节序转换为主机字节序,例如将接收到的端口号转换后使用
注意以上函数区别。
socket编程接口
下面介绍一些常见的socket接口:
C
// 创建 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);
一般来说,使用socket编程通常需要包含四个头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
创建socket套接字
可以使用socket函数来创建一个套接字
#include <sys/types.h>
#include <sys/socket.h>// 创建一个IPv4的TCP套接字
int socket(int domain, int type, int protocol);
- domain:地址簇,常见的有- AF_INET(IPv4),和- AF_INET6(IPv6)
- type:套接字类型,常见的有- SOCK_STREAM(TCP),- SOCK_DGRAM(UDP)
- protocol:协议,通常为0(自动选择),也可以指定协议,如- IPPROTO_TCP或- IPPROTO_UDP。
- 成功时返回一个套接字描述符,失败时返回-1,并设置error
注意这里的套接字描述符其实就是文件描述符,查看man手册我们可以看到:
 
 也就是说,调用socket这个函数得到一个文件描述符,在操作系统的内核中,它对应于一个数据结构,存储了该套接字的各种状态信息和资源,例如IP地址、端口号、通信协议、缓冲区等。
bind绑定套接字
bind函数将套接字绑定到一个IP地址和端口号。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:套接字描述符
- addr:指向- ockaddr结构体的指针,该结构体对象包含了要绑定的地址信息,对于IPv4,使用- sockaddr_in结构体;对于IPv6,使用- sockaddr_in6结构体。
- addr_len:- addr指向结构体对象的大小,通常使用- sizeof获取
- 成功时返回0,失败时返回-1并设置errno
listen建立监听
listen函数使套接字进入监听状态,准备接受连接请求。
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- sockfd:套接字描述符
- backlog:挂起连接的最大队列长度,即最多有多少个连接可以等待被接受
- 成功时返回0,失败时返回-1并设置errno
accept接受连接
accept函数接受一个连接请求,返回一个新的套接字描述符,用于与客户端通信
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:监听套接字描述符,即listen获取的套接字描述符
- addr指向- sockaddr结构体对象的指针,用于存储客户端地址信息
- addrlen:指向- addr指向结构体对象的大小
- 成功时返回新的套接字描述符,失败时返回-1并设置errno
connect建立连接
connect函数用于客户端连接到服务器
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:套接字描述符
- addr指向- sockaddr结构体对象的指针,用于存储服务器地址信息
- addrlen: sockaddr 结构体的大小。
- 成功时返回0,失败时返回-1并设置errno
sendto发送数据
sendto函数是用于在 无连接套接字**(如UDP)上发送数据 的。它允许程序指定目标地址,从而可以在一个套接字上与多个目标进行通信。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
- sockfd: 套接字描述符,通过 socket 函数创建
- buf: 指向要发送的数据缓冲区
- len: 要发送的数据的长度
- flags: 发送标志,通常为0
- dest_add: 指向- sockaddr结构体的指针,包含目标地址和端口号
- addrlen:- sockaddr结构体的大小
- 成功时返回发送的字节数,失败时返回-1,并设置 errno 以指示错误
接收数据
recvfrom 函数用于在无连接的套接字(如UDP)上接收数据,它允许程序获取发送数据的源地址。它常用于UDP服务器接收数据包,并且能够知道数据包的来源。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, 
struct sockaddr *src_addr, socklen_t *addrlen);
- sockfd: 套接字描述符,通过 socket 函数创建。
- buf: 指向存储接收数据的缓冲区。
- len: 缓冲区的长度,即可以接收的最大字节数。
- flags: 接收标志,通常为0。
- src_addr: 指向 sockaddr 结构体的指针,用于存储发送方的地址信息。
- addrlen: 指向 socklen_t 变量的指针,表示 sockaddr 结构体的大小。调用函数时需要设置为 sockaddr 结构体的大小,函数返回时设置为实际地址的长度。
close关闭套接字
跟关闭文件描述符一样,创建的套接字描述符用完之后也需要手动关闭,同样的使用close函数。
#include<unistd.h>
int close(int fd);
- 成功返回0,失败返回-1
sockaddr结构体
sockaddr 结构体用于存储套接字地址信息。该结构体是网络地址结构体的通用形式。在具体使用时,通常会用到特定协议族的派生结构体,如 sockaddr_in、sockaddr_un。各种网络协议的地址格式并不相同.
 
- sockaddr:
struct sockaddr {sa_family_t sa_family;  // 地址族(Address family)char sa_data[14];       // 套接字地址数据(Socket address data)
};
- sockaddr_in:
struct sockaddr_in {sa_family_t sin_family;   // 地址族(AF_INET)in_port_t sin_port;       // 端口号(Port number),网络字节序struct in_addr sin_addr;  // IPv4地址char sin_zero[8];         // 填充字节,使结构体大小与 `sockaddr` 一致
};
- in_addr:
struct in_addr {uint32_t s_addr;  // 32位IPv4地址,网络字节序
};
in_addr 用来表示一个 IPv4 的 IP 地址. 其实就是一个 32 位的整数;