C/S 开发框架之通信层的实现
CSFramework编写的主要目的是,为未来编写相应的服务器/客户端模式下的应用系统提供便利。也就是说,我们的CSFramework将完成最底层的相应工作,使得开发人员在使用此框架时,无需再编写底层操作的代码。
最底层–通信层(Communication)的实现
基于服务器端和客户端,双方之间需要进行信息的交互,也就是说,无论是服务器端还是客户端,都同时作为信息的发送方和接收方,都需要进行信息的传递工作,于是,我们将信息的传递独立出来,形成通信层(Communication),主要负责信息的传递,包括发送信息和侦听对端的消息,以及监控对端异常掉线的情况。另外,通信层需要不断地侦听对端的消息,所以Communication实现Runnable接口来创建一个线程。
public abstract class Communication implements Runnable {
protected Socket socket;
protected DataInputStream dis;
protected DataOutputStream dos;
protected volatile boolean goon;
双工通信三要素:
Socket类的对象,即,通信端口封装类。由它才能创建通信信道,并在结束通信时,关闭网络连接;
DataInputStream类的对象,即,输入通信信道;
DataOutputStream类的对象,即,输出通信信道。
在任何需要直接通信的双方,只要拥有这三个要素,就能进行直接通信。
goon,是线程开始和结束的控制;而volatile 是为了消除寄存器优化。
volatile关键字
下面对volatile 关键字修饰作一些解释:
当编译系统对某个变量进行寄存器优化之后,对于这个变量的读写操作,不是对真正的内存变量空间进行读写操作,而是仅仅对提供了这个变量数据的寄存器进行操作。这就意味着,如果存在两个线程,它们都存在对同一个变量的寄存器优化后,那么两个线程表面上是对同一变量进行的访问,实质上,是对各自的寄存器中的值进行的访问。也就是说,就算某一个线程更改了“同一个变量”的值,本质上只是更改了寄存器的值,并没有真正更改“内存空间”的值,也就是变量的值,从而使得两个线程并没有彼此真正影响!
但是,一旦加了 volatile 关键字,意思就是向编译器说明:这个变量不能进行寄存器优化!所以,如果类的某个成员,会在不同的线程中进行访问,那么,这个成员最好加上 volatile 修饰。
下面来看看 Communication 类的线程代码片段:
@Override
public void run() {
String message = null;
while (goon) {
try {
message = this.dis.readUTF();
dealPeerMessage(new NetMessage(message));
} catch (IOException e) {
if (goon == true) {
dealAbnormalDrop();
goon = false;
}
}
}
close();
}
只要goon为true,这个线程会一直在“后台”不停的运行,不断侦听对端发送的消息,一旦对端异常掉线,message = this.dis.readUTF(); 将立刻产生异常,而另外一种情况,dis 一旦被关闭,也会产生异常。所以为了区分这两种异常,我们在异常捕获块中,对goon的值进行判断,如果goon为true,说明一定是对端异常掉线,这时我们需要对对端异常掉线进行处理,然后令goon为false即可。
接下来解释一下两个抽象方法:
protected abstract void dealPeerMessage(NetMessage message);
protected abstract void dealAbnormalDrop();
之所以用抽象方法来处理消息以及对端异常掉线,是因为这不是Communication应该做的事,而应该是由它的上一层具体处理,即会话层。
一开始我们就说,通信层的主要工作是信息的传递,不涉及具体信息的处理,这样Communication类的功能也就单一化。所以这里我们用两个抽象方法,它的上一层只要继承Communication,就必须实现这两个抽象方法。
网络信息的规范化–协议
在上面线程代码片段中,还有一个值得注意的问题,就是NetMessage.
首先,我们在这里是为了简化网络通信的难度,只是用了 readUTF() 和writeUTF() 这两个方法,接收字符串信息和发送字符串信息。而在实际应用中,还可以有其他方式发送字节信息,比如图片信息,音频信息。另外,用户在使用我们的工具的时候,对于传输的数据,其格式是千差万别的。为此,我们需要将传输的网络信息进行规范化,也能够方便以后反复需要做的网络信息解析工作。
首先是网络命令:
public enum ENetCommand {
SERVER_OUT_OF_ROOM, //服务器连接已满
ENSURE_ONLINE, //客户端确认上线
OFFLINE, //客户端下线
SERVER_FORCE_DOWN, //服务器强制宕机
}
这里面分为,服务器命令和客户端命令,还有部分命令是双方共有的。
那么NetMessage所需要做的工作就是,服务器端和客户端要发送的信息,是由这个类的对象生成的;而在发送和接收时,网络上传输的是这个类对象形成的字符串;在发送前和接收后,应该转换为本类对象。
代码实现如下:
@Override
public String toString() {
//我们希望输出字符串按照一定规格:
//假设三个成员的值为:
//command:CLIENT_LOGIN
//action:clientLogin
//parameter:"id = 123456, password = 54321"
//希望在网络上传递信息格式如下:
//CLIENT_LOGIN:clientLogin:id = 123456, password = 54321
return command + ":" + (action == null ? " " : action) + ":" + parameter;
}
NetMessage(String message) {
int colonIndex = message.indexOf(':');
String commandStr = message.substring(0, colonIndex);
this.command = ENetCommand.valueOf(commandStr);
message = message.substring(colonIndex + 1);
colonIndex = message.indexOf(':');
String actionStr = message.substring(0, colonIndex).trim();
this.action = (actionStr.length() <= 0 ? null : actionStr);
this.parameter = message.substring(colonIndex + 1);
}
Communication完整代码实现:
package com.mec.csframewok.core;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
/**
* 通信层主要负责信息的传递;<br>
* 包括发送信息和侦听对端消息,并初步处理信息;<br>
* 本层还负责监控对端异常掉线情况;
* @author 14947
*
*/
public abstract class Communication implements Runnable {
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(socket.getInputStream());
this.dos = new DataOutputStream(socket.getOutputStream());
this.goon = true;
new Thread(this, "通信层").start();
}
protected void send(NetMessage message) {
try {
this.dos.writeUTF(message.toString());
} catch (IOException e) {
close();
}
}
protected abstract void dealPeerMessage(NetMessage message);
protected abstract void dealAbnormalDrop();
@Override
public void run() {
String message = null;
while (goon) {
try {
message = this.dis.readUTF();
dealPeerMessage(new NetMessage(message));
} catch (IOException e) {
if (goon == true) {
dealAbnormalDrop();
goon = false;
}
}
}
close();
}
protected void close() {
goon = false;
try {
if (dis != null) {
dis.close();
}
} catch (IOException e) {
} finally {
dis = null;
}
try {
if (dos != null) {
dos.close();
}
} catch (IOException e) {
} finally {
dos = null;
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
} finally {
socket = null;
}
}
}
NetMessage完整代码实现:
package com.mec.csframewok.core;
/**
* 网络信息<br>
* 服务器端和客户端发送的信息,需由此类的对象生成;<br>
* 在发送和接收时,网络上传输的是本类的对象形成的字符串;<br>
* 由于本类是工具的内部核心类,所以包权限;
* @author 14947
*
*/
public class NetMessage {
ENetCommand command;
String action;
String parameter;
NetMessage() {
}
NetMessage(String message) {
int colonIndex = message.indexOf(':');
String commandStr = message.substring(0, colonIndex);
this.command = ENetCommand.valueOf(commandStr);
message = message.substring(colonIndex + 1);
colonIndex = message.indexOf(':');
String actionStr = message.substring(0, colonIndex).trim();
this.action = (actionStr.length() <= 0 ? null : actionStr);
this.parameter = message.substring(colonIndex + 1);
}
ENetCommand getCommand() {
return command;
}
NetMessage setCommand(ENetCommand command) {
this.command = command;
return this;
}
String getAction() {
return action;
}
NetMessage setAction(String action) {
this.action = action;
return this;
}
String getParameter() {
return parameter;
}
NetMessage setParameter(String parameter) {
this.parameter = parameter;
return this;
}
@Override
public String toString() {
//我们希望输出字符串按照一定规格:
//假设三个成员的值为:
//command:CLIENT_LOGIN
//action:clientLogin
//parameter:"id = 123456, password = 54321"
//希望在网络上传递信息格式如下:
//CLIENT_LOGIN:clientLogin:id = 123456, password = 54321
return command + ":" + (action == null ? " " : action) + ":" + parameter;
}
}
至此,关于CSFramework的通信层Communication以及网络信息的规范化NetMessage 简述结束。
下一篇将继续CSFramework的会话层的实现。