赞
踩
源IP地址和目的IP地址
源MAC地址和目的MAC地址
总的来说,IP 地址用于标识主机在网络中的位置,MAC 地址用于标识局域网中主机的网络接口。在不同网络之间的通信,通常需要使用 IP 地址引导路由和转发;而在同一局域网内的通信,则需要使用 MAC地址进行直接传输。
端口号(port)是传输层协议的内容:(在OS内)
端口号用于唯一的标识该主机上的一个网络应用层进程,通过端口号可以将网络数据分用给指定的进程。
端口号是一个2字节16位的整数,范围0~65535;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程,标识进程在全网的唯一性
所以网络通信的本质其实就是进程间通信
一个端口号只能被一个进程占用,但一个进程可以绑定多个端口号。
其中 0~1023 的端口号被保留用于一些特定的服务和应用程序,称为“系统端口”或“熟知端口”,例如 HTTP 服务使用的端口号为 80,SMTP 服务使用的端口号为 25。每个端口号都与一个特定的应用程序或服务相关联,用于区分同一主机上的不同应用程序。
进程PID就已经标识了同一台主机上进程的唯一性,为什么又设计了端口号?
客户端和服务端是如何知道对方的IP+端口号用于进行网络通信的?
操作系统如何根据端口号找到指定的进程?
TCP协议
TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。
其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法
UDP协议
使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了
但是UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的,也无法处理
有了TCP可靠传输,为什么还要有UDP?
计算机的存储是分大小端的,在C语言已经学习过了
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分
如果只在一台主机上,那么是不需要考虑大小端问题的,因为同一台机器上的数据采用的存储方式都是一样的,要么采用的都是大端存储模式,要么采用的都是小端存储模式。
但如果涉及跨主机网络通信,那就必须考虑大小端的问题,否则其他主机识别出来的数据可能与发送端想要发送的数据是不一致的
比如,两台主机之间在进行网络通信,其中发送端是小端机,而接收端是大端机。发送端将发送缓冲区中的数据按内存地址从低到高的顺序发出后,接收端从网络中获取数据依次保存在接收缓冲区时,也是按内存地址从低到高的顺序保存的
但由于发送端和接收端采用的分别是小端存储和大端存储
发送端按小端的方式发送数据,比如:0x12345678,发送的顺序是:78 56 34 12,小端识别数据是正常的:0x12345678
而接收端是大端存储,接受数据的顺序是 78 56 34 12,最后识别的数据是:0x78563412
此时接收端识别到的数据与发送端原本想要发送的数据就不一样了,这就是由于大小端的偏差导致数据识别出现了错误
解决方法
总结
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高权值
不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据
如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可
网络字节序与主机字节序之间的转换
为使网络程序具有可移植性,使同样的代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
注:转换工作不需要我们自己做。直接调用库函数即可完成
#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表示 long,32位长整数,s表示 short,16位短整数
htonl的意思就是:把32位的主机字节序转换成(to)32位网络字节序
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回
socket的中文意思是插座,在计算机叫套接字
在进行网络通信时,客户端就相当于插头,服务端就相当于一个插座,但服务端上可能会有多个不同的服务进程(多个插孔),因此当我们在访问服务时需要指明服务进程的端口号(对应规格的插孔),才能享受对应服务进程的服务
套接字的种类大致分三种:
由于有三种套接字,为了使用方便,接口设计者就只设计了一套接口,可以通过传不同的参数,解决所有网络或者其他场景下的网络通信
注:后面只讲网络套接字,域间套接字的使用方法基本相同
// 创建 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);
前面说到设计者只设计了一套接口,这套接口里面最重要的就是sockaddr结构体,除此之外还有两个结构体:sockaddr_in和sockaddr_un
sockaddr_in结构体是用于跨网络通信,其中sockaddr_in的in是inet,“inet” 是Internet Protocol(IP)的简写
sockaddr_un结构体是用于本地通信,其中sockaddr_un的un是unix
但是我们使用函数时,传的参数是sockaddr结构体,并不是sockaddr_in和sockaddr_un
sockaddr结构体的前16位表示地址类型,函数会以传入对应的地址类型解释该结构。
我们需要做的就是对sockaddr_in和sockaddr_un结构体进行填充,然后传参给sockaddr,这个过程必须要强制类型转换
在socket函数的内部,对于传进来的参数sockaddr直接取前两个字节,进行判断是网络通信还是本地通信,知道这个结果后再强制类型转换回sockaddr_in和sockaddr_un结构体
用这样的设计方式,就可以设计出同一套套接字接口了。
为什么不用void*代替struct sockaddr*?
因为当时设计这些socket接口时,C语言的语法还不支持void*。
并且在C语言支持了void*之后也没有将它改回来,因为这些接口是系统接口,直接内嵌在OS里面的,系统接口是所有上层软件接口的基石,系统接口是不能轻易更改的,否则会带来某些严重的后果
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。