要写一个简单的通讯系统,我们首先要明确几点:
一:我们要有一个服务器和多个客户端、服务器和客户端之间通过套接字连接
二:套接字之间通过输入输出流传递信息
三:传递的信息的过程中要做好信息分割。
四:一些技术细节
我们分别来看以上几点:
一:服务器和客户端
java自带一个server类可以新建一个服务器,在新建的时候我们要指定他的IP地址和端口号。然后等待客户端来连接这个服务器(accept方法),等待过程中服务器的主线程会阻塞。
客户端要首先新建一个客户端套接字,并指定套接字的连接地址与端口号,这些要与服务器的地址端口号对应。服务器接受到这个套接字之后会在服务器主线程返回一个服务器套接字。此时再启动一个线程来专门处理这个服务器套接字。
也就是说,要创建一个服务器和客户端的连接,我们需要两个套接字:服务器套接字和客户端套接字。并且分别要一个线程来处理这两个套接字。要注意将服务器套接字的线程与服务器的主线程区别开。
二:套接字之间通过输入输出流传递信息
每个套接字都可以获得相应的输入输出流。这里的输入输出是相对于这个套接字来说的,比如输入流是套接字接受到信息。套接字之间通过这些输入输出流来传递消息。在接下来我们会看到有很多种不同的所谓的输入输出流,但他们都是共用得同一个通道来进行通信的。换句话说全部的输入流共用一个通道,全部的输出流共用另外一个通道。这种输入输出分开的方式叫做全双工模式。
信息传递中的流可以分为基本流和高级流(这里并不包括文件读写之类的IO流)。
基本流有InputStream、OutputStream和Writer和Reader。其中InputStream和OutputStream收发的字节(Byte,8位),Writer和Reader收发的是字符(16位)。我们将发送字节的叫字节流,发送字符的字符流。字节流适合收发二进制文件,比如图像、音频;字符流则自带编码,适合传输文本文件。这两种流并不互通,没有继承关系,但可以通过java提供的桥接类来将他们相互转化,比如InputStreamReader 将 InputStream 转换为 Reader。
高级流是在基本流的基础上通过装饰模式进行增强得到的。所谓装饰模式和工厂模式等一样,是设计模式的一种。在这里我们只需要知道高级流本质上通过基本流进行传输,但实现了更多的功能和方法就可以了。比如BufferedInputStream为输入流增加了一个缓冲区,这样读进来的数据可以先存放在缓冲区中,在缓冲区读满或接受的数据发送完之后一起处理(具体什么时候处理依赖程序的具体实现,比如说也可以提前调用flush方法将缓冲区中的数据进行处理)。这样就避免了每次读取数据都要进行处理的麻烦,增加了程序的效率。
除了刚刚提到的增加缓冲区的缓冲流,还有对象流。对象流允许直接传输一个java对象,比如HashMap,但这里要求对象必须是可序列化的,即对象要实现Serializable接口。在通信网络中常用的就是这 2*2*1+2*1=6个流(输入输出*字符字节*缓冲 + 输入输出*对象)。此外IO流还有数据流打印流等,这些就与通信网络关系不大了。
三:处理好信息传递过程
从上面的内容我们可以看到,不同的流是很多的,并且他们之间基本不能实现互通。比如说BufferedInputStream不应该去读取BufferedWriter发来的数据。虽然有时候这样也能读到数据,但大多数时间读不到我们想要的数据。比如在用BufferedInputStream读BufferedWriter发来的数据时,读到的是BufferedWriter将字符解编码后的字节值,只通过BufferedInputStream无法将他还原成字符。
因此我们应该注意不同流之间的对应,用那哪个流发,就用哪个流收,注意收发之间的顺序。这里我们可以借用一下各大协议的思路,在发送要发送的内容之前先发送一个头部。头部中标明将来要发送的内容类型等内容,这样就可以告诉接收方调用哪个流来收发。
除了不同流之间的对应之外还有一个问题,如果我们用BufferedWriter或者ObjectOutputStream这种可以调用方法标明截止或是自带截止的还好,如果我们调用的是BufferedOutputStream这种没有截止的,就需要我们自己来设定截止来表明不同次的传输。比如说我们在传输多个文件的时候,BufferedOutputStream并不会在文件传完之后自动加上截止符。如果我们不加的话,下一次再传的时候,接收方就会认为接受的仍是上一次的文档,从而造成错误。因此需要我们手动来为不同次的传输加一个截止符。
有三种比较好用的方法:一是在传输结束之后我们手动再发一条消息,比如“EOF”。然后让接收方每次都判断有没有接收到“EOF”这条消息,如果接收到了就停止接受并进行下一步操作。二是我们可以使用try-with-sorces语句。其实就是在原本的try-catch语句的try后加一个括号,括号中新建一个BufferedOutputStream对象,用这个新建的对象来传递数据。这样子数据传递完之后BufferedOutputStream对象就会被清除,接收方也可以接收到消息。由于每次新建对象都是用同一个OutputStream,因此并不会出现问题。这两种方法各有优劣,第一种方法用的是同一个对象,程序开销更小,但需要手动加分隔符,并且如果传输中出现错误就不好处理了。第二种办法用多个对象,程序开销大,但不用加分隔符,每个文件独立,一个传输错误并不影响另一个。除了上述两种方法外,其实还可以有第三种方法。还记得我们在传输文件之前要先加一个头部吗,我们可以把这个头部利用起来。在传输文件的时候不止告知要传输的类型,还告知将要传输文件的大小,这样接收方就可以在接受完指定大小之后停止接受并进行下一步操作。
四:一些技术细节
- 在每次发送是如果不是ObjectOutputStream这种自带结束符的,要添加分隔符。比如BufferedWriter要newLine,上述的BufferedOutputStream要自己定义方法来实现分割
- 建议每次发送后flush一下将缓存区清空,避免一些奇怪的错误