目录
IP地址和端口号
源IP地址和目的IP地址
端口号
源端口号和目的端口号
TCP/UDP协议
网络字节序
大小端
如何定义网络数据流地址
网络字节序和主机字节序的转换
socket编程接口
sockaddr结构
在IP数据包头部中,会有两个地址,一个叫做源IP地址,一个叫做目的IP地址。源IP地址是指发送数据包的主机的IP地址,而目的IP地址是指接收数据包的主机的IP地址。
在互联网中,每个主机都有一个唯一的IP地址,用于标识该主机的身份和位置。在数据包传输过程中,源IP地址和目的IP地址是用于确定数据包的来源和目的地的重要信息,以便正确地将数据包传输到目标主机。
但是,光有IP地址是不够的。假设你现在用微信向你的好朋友发送了一条消息,有了IP地址能将消息发送到对方的机器上。但是还需要一个特定的标识来区分这个数据要给哪一个程序进行解析。
基于上述的问题,引出端口号的概念,通过IP地址+端口号就能将数据发送给指定主机的某一个程序了。主机的唯一性由公网IP标识,而主机上的服务进程由端口号标识。
端口号(port)是传输层协议的内容。
●端口号是一个2字节16位的整数。
●端口号用来标识一个进程,告诉操作系统,当前的数据要交给哪一个进程来处理。
●IP地址+端口号能够标识网络上的某一台主机的某一个进程。
●在同一台主机上,同一个端口号不能标识多个进程。不同主机上的标识各自进程的端口号可以相同。
●一个进程可以绑定多个端口号,但一个端口号不能被多个进程绑定。
小疑问:如果端口号的功能是用来标识一个进程,为什么不用PID呢?两者之间有什么关系。
答:1.如果使用PID,进程每次重新启动,PID都可能会更改,PID变了,网络部分也要进行修改。采用端口号的方式,操作系统是操作系统,网络是网络(解耦)。2.服务器的唯一性不能做任何改变(IP+Port)。3.不是所有的进程都需要提供网络服务或者请求,但是所有进程都需要PID。
源端口号和目的端口号在计算机网络中用于标识数据包发送和接收的进程或应用程序。源端口号是发送方计算机上的端口号,目的端口号是接收方计算机上的端口号。在TCP/IP协议中,源端口号和目的端口号都是16位的整数,取值范围是0~65535。
TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)都是互联网协议中的传输层协议,用于在网络上传输数据。
TCP是一种可靠的、面向连接的协议,它通过三次握手建立连接,保证数据的可靠传输。TCP在传输数据时,会对数据进行分段,并为每个分段进行编号,接收方会对每个分段进行确认,如果发送方没有收到确认,就会重新发送数据。TCP还会对数据进行流量控制和拥塞控制,以避免网络拥塞和数据丢失。
●传输层协议●有连接●可靠传输●面向字节流
UDP是一种不可靠的、无连接的协议,它不会对数据进行分段和编号,也不会进行确认和重传。UDP适用于需要快速传输数据的应用,如实时音视频传输、在线游戏等。由于UDP没有TCP那么多的控制机制,因此它的传输效率更高,但也更容易出现数据丢失和网络拥塞的问题。
●传输层协议●无连接●不可靠传输●面向数据报
总的来说,TCP适用于需要可靠传输的应用,而UDP适用于需要快速传输的应用。需要注意的是上述的可靠和不可靠是中性词,只是特点不同,适用场景不同。
内存中的多字节数据相对于内存地址由大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端。
●发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
●接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
●网络数据流的地址规定:先发出的数据是低地址,后发出的数据是高地址。
●TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
●不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
●如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
小端模式下,数据的低位字节存储在内存的低地址处,高位字节存储在内存的高地址处。
大端模式下,数据的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
在上述的函数名中,h表示host,n表示network。l表示32位长整数。s表示16位短整数。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
// 创建 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);
在我们进行网络通信的过程中,套接字的种类不止一种,比如:网络套接字编程,原始套接字和unix域间套接字。
网络套接字(Socket),它提供了一组用于网络通信的函数和数据结构,使得应用程序可以通过网络与其他应用程序进行通信。通过网络套接字,应用程序可以实现不同计算机之间的数据传输和通信。
原始套接字(Raw Socket),它允许程序直接访问网络协议栈,从而可以发送和接收任意类型的网络数据包,包括未经处理的数据包。相比于传统的套接字接口,原始套接字提供了更高的灵活性和更细粒度的控制,但也需要更多的编程工作和更高的权限。
域间套接字(Domain Socket),它只能在同一台计算机上的进程之间进行通信。与传统的网络套接字不同,域间套接字不需要经过网络协议栈的处理,因此具有更高的性能和更低的延迟。域间套接字通常用于本地进程之间的通信,比如在同一台计算机上的不同进程之间共享数据或者进行进程间通信等。
如果给不同的套接字各自设计一套接口就太麻烦,所以使用sockaddr结构的方式,通过给一套接口传递不同的参数,就解决了所有网络或者其他问题下的通信问题。
下述示意图以sockaddr_in(inet,网络通信)与sockaddr_un(unix,域间套接字)两个套接字类型为例:
在我们想要进行网络通信的时候,填充的是sockaddr_in(inet,网络通信)与sockaddr_un(unix,域间套接字)的字段。在参数传递的时候强转成sockaddr传递。
不管传递的参数是什么,在函数内部都当做sockaddr看待,根据前两个字节在做判断强转回去。这样一来,通过一套接口就可供不同的套接字调用。