但是人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?通过启动的 qq,迅雷,浏览器。 而启动的 qq,迅雷,浏览器都是进程。换句话说,进程是人在系统中的代表,只要把数据给进程,人就相当于就拿到了数据。
但是系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程?这就要在网络的背景下,在系统中,标识主机的唯一性。
在进行网络通信的时候,是不是我们的两台机器在进行通信呢?
日常网络通信的本质:就是进程间通信!!(从应用层上看,不用看网络栈的底层传输)
要进程间通信,就要先把进程标识出来
定义:
特点:
端口号 port: 无论对于 client 和 server,都能唯一的标识该主机上的一个网络应用层的进程
端口号范围划分0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的.
在公网上:
这种 IP+port 的模式,就叫做 socket (插口,插座) ,实现了客户端和服务器的唯二进程进行通信
例如:10086 是一个 IP,工号 12345 的员工就是 port 端口号
💡 端口号 vs pid
pid 已经能标识一台主机上的进程唯一性了,为什么还要搞一个端口号??
就像一个人有身份证号还有工号,不能用工号代替身份证号,是不同场景下的,如果这样那你从公司离职了呢~
下三层通信示意图
我们的客户端,如何知道服务器的端口号是多少?
每一个服务的端口号必须是众所周知的,精心设计,被客户端知晓的
注意:端口号和进程ID都可以唯一表示一个进程, 但是一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
源端口号和目的端口号:
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号 简单来说就是 “
数据是哪个发的, 最后要发给谁
”
TCP(Transmission Control Protocol 传输控制协议):
UDP(User Datagram Protocol 用户数据报协议):
⭕ 传输层同时存在 tcp 和 udp 是为什么呢? TCP协议和UDP协议不存在哪个更好的说法? 可靠和不可靠都是中性词,就像化学中的惰性一样
TCP:银行,支付...(在网络联通的情况下,丢包可找回)
UDP:信息派发,例如直播...
🧀 网络字节序(Network Byte Order),也称为网络字节顺序,是协议中规定好的一种数据表示格式。它用于在计算机网络中进行数据通信时,统一数据的字节顺序,确保数据在不同主机之间传输时能够被正确解释。
🔥 我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,
🔥 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分。 那么如何定义网络数据流的地址呢?
由于我们不能保证通信双方存储数据的方式是一样的,因此网络中传输的数据必须考虑大小端问题。
TCP/IP 协议规定如下:
无论主机是大端机还是小端机,都必须按照 TCP/IP 协议规定的网络字节序来发送和接收数据:
发送端:
接收端:
总的来说,就是如果当前发送主机是小端, 就需要先将数据转成大端;否则就忽略, 直接发送即可
注意事项
网络字节序与主机字节序之间的转换
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,系统提供了四个函数,可以通过调用以下库函数实现网络字节序和主机字节序之间的转换。
这些函数名很好记:
h
表示主机(host)n
表示网络(network)l
表示 32 位长整数s
表示 16 位短整数函数说明
🌄 Socket API 是一层网络编程接口,抽象了底层的网络协议,定义在 netinet/in.h 中。它适用于多种网络通信方式,如 IPv4、IPv6,以及 UNIX 域套接字(用于本地进程间通信)。通过 Socket API,程序可以实现跨网络的进程间通信(如通过IP地址和端口号进行的网络通信),也可以实现本地的进程间通信。
常见通用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);
① (TCP 常见API)
// 开始监听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);
② (UDP 常见API)
// 函数用于在面向数据报的套接字(如UDP套接字)上发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// 用于从套接字接收数据的方法,特别是在使用UDP协议进行数据传输时
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
我们可以看到上面struct sockaddr *addr出现次数挺多的。实际上在网络上通信的时候套接字种类是比较多的,下面是常见的三种:
设计者想将网络接口统一抽象化--参数的类型必须是统一的,底层是一种多态的设计
运用场景:
我们现在知道套接字种类很多,它们应用的场景也是不一样的。所以未来要完成这三种通信就需要有三套不同接口,但是思想上用的都是套接字的思想。因此接口设计者不想设计三套接口,只想设计一套接口,可以通过不通的参数,解决所有网络或者其他场景下的通信网络。
由于不同的通信方式(跨网络或本地通信)有不同的地址格式,套接字使用不同的结构体来封装地址信息:
为了解决这些不同地址格式的兼容性问题,套接字提供了一个通用的地址结构体 sockaddr,用于统一处理不同的地址结构。
🔥
sockaddr 是一个通用的套接字地址结构,它为所有不同类型的通信方式提供了统一的接口。具体通信时,sockaddr 实际上指向特定的地址结构(如 sockaddr_in 或 sockaddr_un),然后通过强制类型转换来区分是哪种通信方式。
这种设计类似于面向对象编程中的“多态”:sockaddr 可以看作一个“父类”,而 sockaddr_in 和 sockaddr_un 是它的“子类”。在程序中,套接字函数接受 sockaddr* 类型的参数,然后根据具体的通信类型进行处理。
sockaddr 结构体
struct sockaddr {
__SOCKADDR_COMMON(sa_); /* 公共数据:地址家族和长度 */
char sa_data[14]; /* 地址数据 */
};
sockaddr_in 结构体(IPv4 套接字地址)
struct sockaddr_in {
__SOCKADDR_COMMON(sin_);
in_port_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[sizeof(struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof(in_port_t) -
sizeof(struct in_addr)];
};
sockaddr_un 结构体(Unix域套接字地址)
struct sockaddr_un {
__SOCKADDR_COMMON(sun_);
char sun_path[108]; /* 文件路径 */
};
sockaddr 作为通用结构,它的前16个比特用于存储协议家族(sa_family 字段)。这个字段用来表明使用的是哪种通信方式:
通过这种设计,Socket API 可以通过统一的函数接口,处理不同类型的地址格式。开发者只需要将具体的地址结构转换为 sockaddr,并设置协议家族字段,套接字函数就能识别出应该进行哪种通信。
🍒 Socket API 的这种设计带来了极大的通用性,使得开发者在同一套代码中可以处理不同的协议类型。例如,函数 sendto() 或 recvfrom() 可以接受 sockaddr* 作为参数,无论是处理 IPv4、IPv6 还是 UNIX Domain Socket,代码都不需要做出太大改动。
int sendto(int sockfd, const void *msg, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
在这个函数中,dest_addr
是一个通用的 sockaddr*
,程序只需根据实际使用的通信方式(如 IPv4 或 IPv6)对其进行强制类型转换即可。
/* Internet address. */
typedef uint32_t in_addr_t;
struct int_addr
{
in_addr_t s_addr;
};
in_addr 是一个32位的整数,用来表示IPv4的IP地址。通信过程中,IP地址通常是通过字符串格式(如 "192.168.1.1")转换为 in_addr_t 类型的数值来表示。
总的来说
在 TCP 和 UDP 通信中,首先要创建一个 Socket 文件描述符,它本质上是一个网络文件。其函数原型为:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:打开一个网络通讯端口,返回一个文件描述符,如果失败,返回 -1。
参数:
domain:协议域,如 AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(Unix域套接字)。
type:套接字类型,如 SOCK_STREAM(字节流,TCP)、SOCK_DGRAM(数据报,UDP)。
protocol:协议类别,通常设置为 0,自动推导出对应的协议,如 TCP/UDP。
在服务器端,必须绑定一个 IP 地址和端口号,以便客户端可以与服务器建立通信。bind()
函数用于将套接字与 IP 和端口号绑定:
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
功能:将指定的 IP 和端口号绑定到套接字,使之监听指定地址。
参数:
socket:套接字文件描述符。
address:存储地址和端口号的结构体指针。
address_len:地址结构体的长度。
在服务器中,调用 listen()
函数使套接字进入监听状态,准备接受连接请求:
int listen(int socket, int backlog);
功能:让服务器套接字进入监听状态,准备接收客户端连接。
参数:
socket:监听套接字描述符。
backlog:全连接队列的最大长度,用于处理多个客户端连接请求。
服务器使用 accept()
从连接队列中提取下一个连接请求,并返回新的套接字用于与客户端通信:
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
功能:获取一个已完成的连接请求,并返回新的套接字用于客户端通信。
参数:
socket:监听套接字。
address:存储客户端的地址信息。
address_len:地址结构的长度。
客户端通过 connect()
向服务器发起连接请求:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:TCP 客户端使用该函数建立与服务器的连接。
参数:
sockfd:用于通信的套接字文件描述符。
addr:服务器的地址。
addrlen:地址长度。
通过 setsockopt()
可以设置套接字的各种属性,例如端口重用等高级功能:
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
功能:设置套接字的选项,如端口重用等。
参数:
sockfd:套接字文件描述符。
level:选项的层次(如 SOL_SOCKET,IPPROTO_TCP 等)。
optname:选项名。
optval:指向设置值的指针。
optlen:设置值的长度。
IP 地址可以以字符串或整数形式存在。常见的地址转换函数包括:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
#include <sys/types.h>
#include <sys/socket.h>
sendto() (UDP)
用于在 UDP 协议下发送数据:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
send() 用于在 TCP 协议下发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据到指定地址。
参数:
sockfd:套接字文件描述符。
buf:要发送的数据。
len:数据长度。
#include <sys/types.h>
#include <sys/socket.h>
recv() 用于接收 TCP 协议下的数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recvfrom() (UDP)接收来自远程主机的数据:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据。
参数:
sockfd:套接字文件描述符。
buf:存放接收数据的缓冲区。
INADDR_ANY
🔥 在服务器端,INADDR_ANY
(0.0.0.0)可以让服务器监听所有可用的网络接口,而不必指定具体的 IP 地址。这种方式提高了代码的可移植性。
local.sin_addr.s_addr = INADDR_ANY;
Listening Socket vs Connected Socket
TCP 通信流程
TCP vs UDP
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,后面我就要进行【Socket 套接字编程】的内容实战啦,请持续关注我 !!