文章目录
- P1 CSFramework简介
- 1 CSFramework功能概述
- 2 CSFramework的分层
- 3 CSFramework的工作原理
- (1) 客户端如何连接到服务器
- (2) 多个客户端连接到服务器
- (3) 一条私聊消息如何发送出去
- P2 网络中消息的存在形式
- 1 信息 = “信令” + “来源” + “目标” + “消息”
- 2 信令 ENetCommand枚举
- 3 有效的网络信息 NetMessage类
- P3 通信层的构建
- 1 需要通信层的原因
- 2 通信层的实现
- (1) INetConfig接口
- (2) Communication抽象类
P1 CSFramework简介
1 CSFramework功能概述
有了网络编程基础部分的内容,接下来要考虑到“一对多”的连接,即一个服务器对应多个客户端,CSFramework不是一个APP,它是为开发CS模式的APP软件所提供的一整套工具软件
这个工具要实现CS模式下APP要解决的有关网络通信,连接,信息传输,上下线,异常处理等普遍问题,使得基于CSFramework基础上编写的APP,无论是聊天室还是棋牌室,都不用关心底层的问题,可以将主要精力放在APP业务逻辑上
2 CSFramework的分层
对于一个基本功能完备的CS框架,一定要结构分明,条理清晰,对功能进行严格的分层,像MVC开发模式一样,这里做的简单CS框架分为四层:
通信层:负责发送信息和侦听信息(Communication)
会话层:负责完成通信协议(ServerConversation,ClientConversation)
API层:提供各种实用方法供APP调用(Server,Client)
APP层:调用API层的方法完成需求
3 CSFramework的工作原理
(1) 客户端如何连接到服务器
1,Server有着一个线程体用来持续不断地侦听是否有连接请求
2,某Client发起连接,先产生一个该Client的ClientConversation,ClientConversation继承于Communication,拥有发送信息和侦听对端信息的能力
3,Server的线程体侦听到了连接请求,为该Client产生一个ServerConversation,ServerConversation继承于Communication,拥有发送信息和侦听对端信息的能力
ServerConversation的对端就是ClientConversation,反之ClientConversation的对端就是ServerConversation
可以想象你是一个某商家管理客服的组长,每当有一个客户打电话过来,就指定一个工作人员来接听他的电话,你就是Server,这个工作人员就是ServerConversation,专门为客户Client服务,通信信道就是Communication
(2) 多个客户端连接到服务器
一个服务器连接多个客户端,每有一个客户端连接到服务器,在服务器一侧生成一个专门为该客户端服务的ServerConversation,在客户端一侧生成一个与服务器交流的ClientConversation:
(3) 一条私聊消息如何发送出去
对于CS框架,如果未来要作为聊天室的基础,如何将一条消息私聊出去,即:
客户端A 私聊 客户端B 信息message
假设API层私聊方法是toOne(),在会话层实现了toOne()方法
信息在框架中流动方式大致如下:
P2 网络中消息的存在形式
在开始实现底层前,先考虑一个最基本的问题,网络中信息的存在形式该是什么样子,怎么能让服务器正确分辨并处理一条信息
1 信息 = “信令” + “来源” + “目标” + “消息”
假如未来采用CSFramework制作一个聊天室,那么如果信息采用纯字符串在网络中传输的话,试想出现下面的问题:某客户端向服务器发送一条信息“我要下线了”,那么此时服务器应该怎么处理该信息,是将该用户下线,还是将这句话发给某一个客户端,还是发给所有客户端
为了解决这个问题,让服务器能分辨清信息的内容,就需要制定统一的协议,无论是服务器还是客户端所发出的信息,都必须遵守协议
这里采用简陋的“信令” + “来源” + “目标” + “消息”来作为通信中的信息,所谓信令,即已经规定好的一系列字符串,如上线,下线,私聊,群发等,来源即发送者,目标即接收者,消息即具体发送的内容,对于信息如:
私聊:客户端1:客户端2:我要下线了
这样处理信息清楚明了,就不会出现服务器不能辨别信息并正确处理的问题
2 信令 ENetCommand枚举
对于信令,这里采用枚举的方式来完成,即一系列有实际意义的字符串:
3 有效的网络信息 NetMessage类
有了信令,那么就可以规定传输的信息NetMessage,NetMessage类需要四个成员及其对应的set,get方法
command
source
target
message
在私聊过程中,对于发送者而言,对于一个私聊方法toOne(…),由于已知是私聊,那么信令确定,来源即该客户端也确定,那么toOne方法的参数只需要target和message,先创建一个NetMessage类,通过set方法更改其中的command,source,target,message就可以产生一个清晰的NetMessage再通过通信层发送给对端即可:
package com.mec.csframework.core;
public class NetMessage {
private ENetCommand command;
private String source;
private String target;
private String message;
/**
* NetMessage类中的所有方法都设置为包权限
* 因为NetMessage是框架中的一环,不想提供给APP层直接使用
*/
NetMessage() {
}
NetMessage(String netMessage) {
int index = netMessage.indexOf(":");
String strCommand = netMessage.substring(0, index);
this.command = ENetCommand.valueOf(strCommand);
netMessage = netMessage.substring(index + 1);
index = netMessage.indexOf(":");
this.source = netMessage.substring(0, index);
netMessage = netMessage.substring(index + 1);
index = netMessage.indexOf(":");
this.target = netMessage.substring(0, index);
netMessage = netMessage.substring(index + 1);
this.message = netMessage;
}
/**
* 对于这四个成员的set方法全部返回NetMessage类对象
* 以便链式调用set方法,传递一个完整的NetMessage
*/
ENetCommand getCommand() {
return command;
}
NetMessage setCommand(ENetCommand command) {
this.command = command;
return this;
}
String getSource() {
return source;
}
NetMessage setSource(String source) {
this.source = source;
return this;
}
String getTarget() {
return target;
}
NetMessage setTarget(String target) {
this.target = target;
return this;
}
String getMessage() {
return message;
}
NetMessage setMessage(String message) {
this.message = message;
return this;
}
@Override
public String toString() {
StringBuffer str = new StringBuffer();
str.append(this.command).append(":")
.append(this.source).append(":")
.append(this.target).append(":")
.append(this.message);
return str.toString();
}
}
P3 通信层的构建
1 需要通信层的原因
结合网络编程基础,可以发现,一个功能完备的聊天室起码要实现多轮的信息交互,而在one2one模式中的Server和Client,都基于网络通信,且Server的输入通信信道和客户端的输入通信信道,都必须要随时接收到对端发送的消息,这个持续不断的侦听过程,必须不间断,直到通信结束
且对于dis.readUTF()方法的执行本身是阻塞的,像Scanner一样,如果采用顺序的编程方法,当readUTF()方法没有侦听到消息时,那么其后的代码永远无法执行
对于上述的两个问题,都需要用线程解决,因为服务器每和一个客户端连接后,之后都要进行信息的发送或接收,那么就可以将通信最基础的部分组合在一起组成Communication类
2 通信层的实现
(1) INetConfig接口
在网络编程中,需要频繁使用服务器的IP地址和双方确定好的端口号,为了不在每个需要的地方都写一遍,实现代码复用,这里创建一个简单的接口,包含连接的基本信息,并提供一个关闭连接的方法
(2) Communication抽象类
Communication类作为通信层,它需要完成至少建立连接,接收对端的信息,发送信息等功能
且Communication类只负责建立连接,侦听信息,发送信息这些功能,对于将接收到的信息如何处理这种问题,应该留给更高层去完成,所以Communication类还必须作为抽象类,提供几个抽象方法,如处理信息,处理异常情况:
package com.mec.csframework.core;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public abstract class Communication implements Runnable, INetConfig{
protected Socket socket;
protected DataInputStream dis;
protected DataOutputStream dos;
//goon用来控制侦听对端线程的运行
private volatile boolean goon;
//根据一个连接socket,得到输入,输出信道,并启动侦听对端线程
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();
}
//发送信息到对端,信息采用NetMessage类
protected void send(NetMessage netMessage) {
try {
//将NetMessage转换成 信令:来源:目标:消息 发送到对端
this.dos.writeUTF(netMessage.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 留给上层完成的抽象方法
*/
//对端异常掉线
public abstract void peerAbnormalDrop();
//具体处理消息
public abstract void dealNetMessage(NetMessage netMessage);
//侦听对端线程,持续不断的侦听对端发来的消息
@Override
public void run() {
String message = null;
while(this.goon) {
try {
message = this.dis.readUTF();
dealNetMessage(new NetMessage(message));
} catch (IOException e) {
if(this.goon == true) {
this.goon = false;
peerAbnormalDrop();
}
}
}
//goon=false后,断开连接
close(this.dis, this.dos, this.socket);
}
}