在我的上一篇博客(初识网络)中介绍了一些对于网络的基础知识,这篇博客,我会再介绍一点关于网络的认识,然后直接开始简单的网络编程,也就是socket编程,但是在编程前我们还需要做一些基础的认识。
1. 前置准备
a. 端口
我前面说网络通信实际上就是远距离的两台电脑之间进行数据传输,这句话是对的,但是不完全对,当你在音乐软件上听一个没有听过的音乐时,此时你这端的音乐软件就会向服务器发出请求,然后服务器把这首音乐给你发过来,在这其中确实是两台机器在通信,但是再具体点实际上是两个进程在通信,即你的音乐软件客户端和服务器所运行的程序,那么网络通信实际上是两个进程在通信,我们可以将这种通信方式也理解为进程间通信。
那既然是进程间通信,我们可以使用IP地址找到目标主机,但是我们怎么找到目标进程呢?找到目标进程的方式很多种,因为进程的内核数据结构能标记唯一进程的字段也很多,但是为了实现进程管理和网络之间的解耦,网络协议中就提出了端口的概念,能够实现网络通信的进程都会有一个属于自己的端口,且在进程中唯一。这样我们就可以定位到一台主机上的一个进程了。
端口是一个两个字节的无符号短整型,而IP地址+端口port的组合就叫套接字也就是socket。
对于端口的认识我们要知道,为了表示端口在进程中唯一,那么一个端口是不可以关联多个进程的,但是一个进程是可以关联多个端口的。
b. UDP/TCP
在传输层中,有两个主要的协议就是udp和tcp,它俩之间的区别就是udp是不可靠的一种网络通信协议,而tcp是可靠的,可靠的原因就是,它能保证数据能无损的传输到目标主机中,但是这种保证也意味着效率的低下,所以关于两种协议的可靠与不可靠就像多线程中函数的可不可重入一样,没有好坏,只是它们自己的一个特点而已。
c. 网络字节序
我们知道,一台机器是有大端和小端的区分的,这说的是数据在内存中如何存储,比如现在有一个数据0x11223344,要存在内存中,假如存在小端机中的话就是这个样子:
而如果是大端的话则是这样:
而网络中的数据传输遵循这样的规则:
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
这就会导致假如服务端机器是小端机,客户端机器是大端机的话,数据就会出现读取错误,造成这种的原因就是大小端之间没有谁更好的说法,所以不同的存储介质厂商也就会采用不同的存储方式,所以这时候网络协议就站出来了,无论你的机器是大端机还是小端机,我要求你给我发过来的网络序列必须是大端格式的,这样就解决了这个问题,这就是网络字节序。
而在关于使用网络通信的接口时IP和端口是必要的,系统就提供了这样的接口:
它可以将本地的ip地址和port端口转化为网络字节序列,也可以将网络字节序列转化为本地IP地址和port端口。
2. socket编程——udp协议
a. 本地网络测试
接下来我们就可以进行socket编程了,首次进行编程我们只进行简单的数据传输,我们首先介绍udp协议的socket编程:
1). 服务端
我们都知道在Linux中有着一切皆文件的说法,那么网络通信实际上也是一种文件的IO,所以我们首先就要建立socket文件描述符:
其中第一个参数只需要填一个宏就可以了:
表示它进行网络间通信,
第二个参数填这个宏,表示是以数据段作为对象传输的,也代表使用udp协议,而第三个参数默认填0就可以,因为前两个参数已经能够确定了:
之后我们就需要将这个线程跟IP地址和端口关联起来,让它成为网络线程:
而在这个函数中,第二个参数我们需要着重介绍。
结构体介绍
其实socket编程是有很多方式的,这一点从socket函数也能体现出来(不同的宏),实际上socket编程可以分为三类:
Unix socket,它是一种域间socket编程,也就是一台机器上的内部通信(就跟命名管道差不多)。
而第二种就是网络socket,它是通过套接字(IP地址+port端口)的方式进行网络间通信。
第三种是原始socket,主要是用来编写一些网络工具。
这其中我们主要是网络socket。
可以看到socket编程实际上分很多种,但是socket的API却只有一套:
这其中这个struct sockaddr类型的结构体就内涵玄机了,上面的接口中看似传的结构体指针类型是struct sockaddr*的,其实它还有两个类型:
struct sockaddr_in和struct sockaddr_un,这两个结构体与上面结构体的关系如下:
在实际使用socketAPI过程中,传的是下面这两个结构体的指针,区分这两个结构体的地方就是它们的开始的字段domain,其中struct sockaddr_in是用于网络socket的结构体,struct sockaddr_un是用于Unix socket的。
我们发现虽然它们使用的API一样,但是通过一个同一类型的指针访问到第一个domain字段就能分辨出到底是实施哪种socket编程方式,其中struct socket就好像基类,struct sockaddr_in和struct sockaddr_un像子类,这其中的关系就好像多态一样,而这就是C语言风格的多态。
所以我们就来认识struct sockaddr_in这个结构体:
这个结构体中框框之外的字段就是填充字段,我们现在不会用到,我们使用的是框框中的字段。
我们再来看一下bind;
它的第二个参数就是一个结构体,我们实际要传的就是struct sockaddr_in类型的结构体,也就意味着我们需要填充好struct sockaddr_in中的内容,第三个参数就是我们实际要传的结构体的大小:
在这里,我们需要注意三点,一点是结构体中的IP地址是一个四个字节的无符号网络字节序,而我们的IP地址为了方便阅读,采用的是字符串的方式这其中的转换怎么做?还有一点在我们看结构体struct sockaddr_in时并没有发现它有sin_family字段,那么这个字段是什么?还有我们并没有将端口号编程网络字节序列。
我先来回答第二个问题,这个字段时网络协议家族的意思,传输的依然是我们socket中domain中的宏,这里填充AF_INET意思也是网络socket的意思,那为什么结构体中没有出现这个字段,原因在这里:
第一个问题,其实系统已经提供了对应的接口inet_addr:
它可以将一个字符串形式的IP地址转化为四字节的网络字节序列:
第三个问题也很好解决,接口我们已经介绍过了:
最后还有一个问题,那就是在填充结构体字段之前,需要先初始化一下结构体: