从这篇文章开始我就要给大家开一个新坑了,有关java中网络编程的一些东西,当然我不会在这里面讲一些特别基础的东西,像socket,serversocket这种大家在很多网站上都能查到的东西,我想讲的是一个框架,基于这个框架我们可以开发一个类似于聊天室的东西,通过这个框架的编写可以极大的提升大家对java的理解以及大家撸代码的能力。
1、什么是C/S模式?服务器-客户机,即Client-Server(C/S)结构。C/S结构通常采取两层结构。服务器负责数据的管理,客户机负责完成与用户的交互任务。
2.如何分层?我们在开头的时候说了我们的目的是可以开发一个类似于聊天室的东西,那么,主要任务就是用户之间的通信,而且我们又是基于C/S模式开发的,所以我准备将我们要编写的框架分为以下几个层面:
会话层 (通信层Communication):一切消息的发送,如服务器对客户端发送消息,客户端对客户端发送消息都要通过这一层来完成,而且这个会话层要同时兼容服务器与客户端,因为两者发送消息的性质是不一样的。服务器层(Server):作为整个框架最核心的部分,他拥有对所有客户端的最高控制权,而且它要完成的逻辑是非常繁琐的,要做到对所有客户端的统一管理。客户端层(Client):这一层其实是非常抽象的,因为我们编写的只是一个框架,说白了就是一个工具我们在编写的时候是没有办法知道未来APP层的编写人员会对我们客户端的一些响应做出的对策,这时候我们就应该另辟蹊径,找一条方便简单的路来走,具体是什么我后面会提的。 接下来就是我们首先要完成的,最基础的会话层(通信层):
对于会话层我们先要知道我们要干啥,这个会话层应该有哪些功能供上层使用;
1、首先就是最最主要的功能–发送消息
2.关闭这个会话层
3.开启一个侦听线程,主要侦听来自对端的消息发送并且接收消息
我们先来做好准备工作,既然是通过网络进行通信的那我们肯定是需要一个Socket的,要进行消息的发送接收那我们就需要准备好输入输出通信信道,这里我们通过流的方式进行信息的传递,所以DataInputStream和DataOutputStream也是要准备好的,线程的开关也需要一个开关来方便我们控制所以也需要一个boolean类型的变量
protected Socket socket;protected DataInputStream dis;protected DataOutputStream dos;protected volatile boolean goon;protected Communication(Socket socket) throws IOException {this.socket = socket;this.dis = new DataInputStream(this.socket.getInputStream());this.dos = new DataOutputStream(this.socket.getOutputStream());this.goon = true;new Thread(this, "LOL只会玩提莫").start();}
相信细心的同学已经发现那个boolean类型的goon前面加了一个限制符,这个限制符的作用就是在频繁对goon进行操作时防止寄存器优化。
准备工作做完后,我们思考一下第一个问题,发送消息,这个消息包含的内容可就很多了,如果笼统的将发送的消息类型定义为String类型的话,肯定会造成消息混乱的,因为一条消息中包含的信息真的很多,就拿“单发”这个操作来举例,肯定是客户端告诉服务器我是单发操作,我要发给谁,发什么消息,这样的话一条消息中包含了三个信息,现在来看的话用String作为类型确实是很欠妥的一种做法,所以我们将这个消息专门用一个类封装起来。
这个类中包含了消息所含的所有信息(还是以单发为例)<1>命令command-单发,<2>动作action-发给谁,<3>真正的消息message-发什么消息。而且命令是我们规定好的,所以我们可以用一个枚举来定义这些个命令。
public enum ENetCommand {OUT_OF_ROOM,ID,OFFLINE,FOREC_DOWN,TO_ONE,TO_OTHER,TO_SP_OTHER,REQUEST,RESPONSE,}
有了这个我们就再将那个用来封装消息的类进行完善:
public class NetMessage {private ENetCommand command;private String message;private String action;public NetMessage() {}public ENetCommand getCommand() {return command;}public NetMessage setCommand(ENetCommand command) {this.command = command;return this;}public String getMessage() {return message;}public NetMessage setMessage(String message) {this.message = message;return this;}String getAction() {return action;}NetMessage setAction(String action) {this.action = action;return this;}}
大家可以看到我所有set方法的返回值类型都是NetMessage,这是为什么呢?如果不这样做可以吗?
第一,如果不这样做当然是可以的
第二,这样做的话我们可以通过new一个匿名对象进行链式调用
这样写的好处在下一篇文章中大家就可以看到了。
对消息做了这么多的准备工作了,我们就可以很简单的编写我们的发送消息这个发法了:
protected void send(NetMessage netMessage) { //规定消息格式,避免混乱try {this.dos.writeUTF(gson.toJson(netMessage));} catch (IOException e) {e.printStackTrace();}}
咦,为啥会有gson这个东西呢?这是因为我们的writeUTF这个方法的参数是String类型的,而我们传进来的参数是自定义类型的,所以要通过gson将我们这个对象转换为一种标准格式的json字符串,那这个方法怎么初始化呢–请看->
public static final Gson gson = new GsonBuilder().create();
使用这个需要导一个Gson的一个包,大家可以在官网上找找,也可以评论或者私信我。
至此,我们用来发消息的send方法编写到此结束。
关闭操作其实看起来是比较简单的,就是将输入通信信道,输出通信信道以及socket关闭,但是如果发生异常,或者进行重复关闭时我们的程序还能不能正常的完成关闭操作呢?
protected void close() {this.goon = false;try {if (this.dis != null) {this.dis.close();} } catch (IOException e) {} finally {this.dis =null;}try {if (this.dos != null) {this.dos.close();}} catch (IOException e) {} finally {this.dos = null;}try {if (this.socket != null && !this.socket.isClosed()) {this.socket.close();}} catch (IOException e) {} finally {this.socket = null;}}
在这里面我们先将控制线程持续运行的goon赋值为false,关闭侦听线程
然后再检测dis和dos有没有为null,如果为null的话我们什么都不做,如果不为null的话我们就将dis和dos关闭,可以看到我在finally中将dis和dos赋值为了null,这样就算出现异常我们的程序也是可以将dis和dos进行合理的关闭,socket的处理方案和上面dis和dos是一样的。
开启侦听线程,侦听对端消息:@Overridepublic void run() {//线程的主要作用就是侦听有无消息传来,并对消息进行处理String message = "";while(this.goon) {try {message = this.dis.readUTF();dealNetMessage(gson.fromJson(message, NetMessage.class));} catch (IOException e) {// 此时的IO异常可能是因为对端异常掉线,没有正常的使用close方法,所以// goon并没有被赋值为falseif (this.goon == true) {this.goon = false;peerAbnormalDrop();}}}}
首先就是将发送的信息读取到,但是别忘了我们传递过来的是一个Json字符串,要先将它转换成一个NetMessage类型的对象,才能对传递过来的信息进行正确合理的解析。
我们可以看到这里面有两个我从未提到的函数:
dealNetMessage();函数和peerAbnormalDrop();函数
为什么我在communication层里面没有提到过呢?因为这两个函数并不是我在communication层里面应该做的事情!
dealNetMessage();指的是对发送过来消息应该进行怎样的处理,我们这个communication层只做最基本的通信工作,至于如何处理消息应该是会话层做的事;peerAbnormalDrop();指的是对端异常掉线,这个的处理应该也是在会话层处理的事情也不是我们communication层应该做的事情。但是现在的逻辑确实需要需要这两个东西,怎么办呢——抽象方法!!!
将这两个函数定义为抽象方法后面APP层的编写只需要实现我们的这两个方法就行了,这样整体的逻辑就完整了,这就是抽象方法的作用!
本章的最基本的会话层编写到这就结束了,后续的服务器和客户端的编写会很快更新出来的,如果有大佬觉得我写的不够正确,欢迎批评指正!
最后附上源码,仅供参考:
public abstract class Communication implements Runnable{public static final Gson gson = new GsonBuilder().create();protected Socket socket;protected DataInputStream dis;protected DataOutputStream dos;protected volatile boolean goon;protected Communication(Socket socket) throws IOException {this.socket = socket;this.dis = new DataInputStream(this.socket.getInputStream());this.dos = new DataOutputStream(this.socket.getOutputStream());this.goon = true;new Thread(this, "LOL只会玩提莫").start();}public abstract void dealNetMessage(NetMessage netMessage);public abstract void peerAbnormalDrop();protected void send(NetMessage netMessage) { //规定消息格式,避免混乱try {this.dos.writeUTF(gson.toJson(netMessage));} catch (IOException e) {e.printStackTrace();}}@Overridepublic void run() {//线程的主要作用就是侦听有无消息传来,并对消息进行处理String message = "";while(this.goon) {try {message = this.dis.readUTF();dealNetMessage(gson.fromJson(message, NetMessage.class));} catch (IOException e) {// 此时的IO异常可能是因为对端异常掉线,没有正常的使用close方法,所以// goon并没有被赋值为falseif (this.goon == true) {this.goon = false;peerAbnormalDrop();}}}}public void close() {this.goon = false;if (dis != null) {try {this.dis.close();} catch (IOException e) {} finally {this.dis = null;}}if (dos != null) {try {this.dos.close();} catch (IOException e) {} finally {this.dos = null;}}if (this.socket != null && !this.socket.isClosed()) {try {this.socket.close();} catch (IOException e) {} finally {this.socket = null;}}}}