目录
前言
1.理解源IP地址和目的IP地址
2.理解端口号
2.1端口号与进程pid的关系
2.2源端口号和目的端口号
3.协议
3.1TCP协议
3.2认识UDP协议
4.网络字节序
5.socket编程接口
总结
在上一篇文章网络框架中给大家对网络的整体进行了一个宏观的介绍,这篇文章中我们进一步来看看两台主机是如何实现网络数据通信的。
在实现不同主机通信的时候首先需要知道对方的IP地址,就如同是唐僧从东土大唐向西天取经,东土大唐可以理解为是源IP地址,而西天可以理解为是目的IP地址,每一台主机实现数据通信的前提是需要有IP地址。
如何查看自己主机上的IP地址:ifconfig
当我们有了对方的IP地址之后,经过中间的不断路由此时到达了对方的主机,但这样就能够实现数据通信了吗?答案是数据到达对方的主机只是完成了第一步,并没有真正完成数据通信,就如同是唐僧到达西天并没有取到经,要想真正取到经是要去大雷音寺的,所以数据到达主机之后,真正通信的是主机上的进程,所以如果找到主机上的进程呢?
为了标识进程的唯一性,在网络通信中提出了端口号的概念,用端口号唯一找到一个进程。此时,有了IP地址加上端口号,就可以在全网上标识一个唯一的进程了,进而在网络数据通信的时候就可以准确的实现数据传输了。
端口号的是一个2字节16位的整数,标识主机上的一个进程,此时可能会有细心的小伙伴会想,既然端口号是唯一的标识主机上的一个进程,那进程的pid不就是唯一标识一个进程的吗?为什么还要引入端口号呢?为什么不直接复用pid呢?
答案是在技术上这种方案是可以的,但是引入端口号的原因是也是考虑了以下几个原因:
a.为了网络与系统解耦
b.一般服务器的端口号是不能轻易改变的,进程在退出后,重启再次启动该进程,进程的pid就发送改变了
c.不是所有的进程都会进行网络服务和请求,但是所有的进程都需要有pid
底层操作系统是如何根据port找到找到对应的进程?
操作系统为port维护了一个hash表作为key值,而value是进程PCB
注:一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;
在理解了端口号之后,所谓的源端口号就是数据是发送方主机的哪一个进程发的,而目的端口号就是接受方主机的哪一个进程。
在上一篇文章中我们介绍了实现网络数据通信必须要遵守协议约定,在今天对协议可以进一步明确,有了IP地址和port端口号,为实现数据通信提供了技术支持,那在实现双方通信的时候需要告诉对方自己的IP地址和port端口号呢?答案是显然需要的,那如何告知对方呢?
就需要使用协议了,在发送数据的时候协议报头中填充对应的IP地址和port端口号这些字段。
在这里我们主要介绍两种协议,一种是TCP协议,一种是UDP协议。
此处先对TCP(传输控制协议)有一个直观的认识; 后面我们再详细介绍。
传输层协议
有连接
可靠传输
面向字节流
此处我们也是对UDP(用户数据报协议)有一个直观的认识; 后面再详细讨论.
传输层协议
无连接
不可靠传输
面向数据报
注:关于TCP协议和UDP协议的具体细节在后面进行介绍,看完上面的简单介绍,相信大家有一个直观的认识TCP是可靠传输而UDP是不可靠传输,可能好奇的小伙伴就会问,既然是不可靠传输,那为什么还保留呢?用可靠传输不是更好吗?
答案是关于可靠和不可靠在网络这里是一种中性词,不是说可靠就一定好,而不可靠就一定不好关于可靠和不可靠是根据不同的场景决定的。可靠的实现是有一定成本的,在维护和编码上是比较复杂的,而不可靠就意味着在维护和编码上更加简单。在网络数据传输的过程中不允许出现数据丢失的情况此时就可以采用TCP协议,但是在数据传输的过程中允许有一些报文丢失,就可以使用UDP
有了上面的技术支持,我们就可以实现双方数据通信了,接下来就需要考虑数据在具体发送时的一些问题,前面我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
使用说明:
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
作为程序员,实现网络数据通信我们只需要关心应用层的开发,关于传输层和网络层是由OS帮我们维护的,既然底层是由OS系统维护的,我们必然要调用操作系统为我们提高的接口,下面我们就来一起认识一下网络编程中所使用的接口
// 创建 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);
关于每个接口的具体参数我们在后面编码的时候再进行介绍,在这里我们主要介绍一下struct sockaddr*,很明显这是一个结构体,那这个结构体具体是什么呢?
关于socket编程主要分为三类
a.网络套接字编程
b.原始套接字
c.unix域间套接字
第一种可以实现跨主机和本地通信
第二种可以可以直接绕过传输层和网络层,直接访问数据链路层,主要是用于抓包工具的实现
第三种是实现本地通信
关于这三种方式在后续中主要介绍网络套接字编程。此处虽然有三种通信方式,但是显然在上面我们只介绍了一套接口,那他们都适用吗?答案是适用,那是如何做到的呢?
在使用的时候传递不同的结构体,初始时为struct sockaddr,如果是网络套接字编程就强转为struct sockaddr_in,如果是unix域间套接字就强转为sockadd_un
如图所示:
有了上面知识之后,接下来我们就可以编码完成实现不同主机数据通信,关于如何编码完成,将在下一篇文章中为大家介绍。