TCP服务器端负责交换控制端和被控制端的数据,采用多端口模式设计,但是目前测试只使用一个端口。
具体流程为
step1:服务器QServer启动tcp服务线程,等待客户端链接
step2:当有客户端链接进来,创建一个客户端对象,存储到静态map里面,key为客户端识别号
step3:客户端链接成功,首先必须发送识别码,获取识别码以后,去数据库查询有没有相关的链接对象,如果有,查询对方的识别号,吧客户端对象存储到全局map,如果没有,直接关闭链接。
step4:在获取数据线程里面,一旦获取到数据,查询对方的id在静态map里面有没有出现,如果出现,就把数据转发过去。
QServer类主要实现服务器端监听,停止,客户端对象创建,销毁等功能,具体代码:
***
* 通讯功能实现
* @author qujia
*
*/
public class QServer {
private static final Logger LOG = LoggerFactory.getLogger(QServer.class);
/**
* 存储服务器端对象集合,考虑到多个端口情况。因此使用一个map存储,key为端口字符串
*/
private static Map<String, QServer> servers=new HashMap<>();
private List<QClient> clients=new ArrayList<QClient>();
/**
* 统一的获取服务器的方法
* @return
*/
public static QServer getInstance(int port) {
String key=""+port;//变成字串
if(servers.containsKey(key)) {
//如果已经存在了
return servers.get(key);
}
else {
//创建新的服务器端
QServer s=new QServer(port);
servers.put(key, s);
return s;
}
}
/**
* 端口号
*/
private Integer port;//端口
private boolean isFinished;//服务器是否结束
private ServerSocket svr;//server对象
/**
* 构造方法,不能直接访问,需要通过getInstance访问
* @param port
*/
QServer(int port)
{
this.port=port;
isFinished=true;
this.start();//开始
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
} public void removeClient(QClient c) {
try {
LOG.info("client off line "+c.getAddress());
//clients.remove(c);
c.remove();
clients.remove(c);
}
catch (Exception e) {
// TODO: handle exception
}
}
/**
* 开始
*/
public void start()
{
if(!isFinished)return;//防止重复启动
LOG.info("start svr on port"+this.port);
isFinished = false;
try {
//创建服务器套接字,绑定到指定的端口
svr = new ServerSocket(port);
//等待客户端连接
while (!isFinished) {
Socket socket = svr.accept();//接受连接
//创建线程处理连接
QClient c = new QClient(socket);
c.start();//开始接收
clients.add(c);//该服务器端的集合存一下
}
} catch (IOException e) {
isFinished = true;
} }
/**
* 停止
*/
public void stop()
{
isFinished = true;
for (QClient c : clients) {
c.remove();//停止
}
try {
if (svr != null) {
svr.close();
svr = null;
}
clients.clear();
} catch (IOException e) {
e.printStackTrace();
} }
/**
* 重新开始
*/
public void reStart()
{
stop();//先停止
start();//开始
}
}
QClient主要实现客户端链接进来,会话判断,以及数据转发,主要代码:
/***
* 客户端线程类
* @author qujia
*
*/
public class QClient extends Thread {
private static final Logger LOG = LoggerFactory.getLogger(QClient.class);
/**
* 存储客户端集合,key为客户端识别号
*/
public static Map<String,QClient> clients=new HashMap();//
/**
* 查询会话使用,这个是不能用autowared,因为这个对象不是springcontenxt创建的
*/
SessionService sessionSvr;
/**
* socket对象
*/
private Socket socket;
private InputStream in;
private OutputStream out;
byte[] buffer = new byte[1024];
private String clientID;//客户端识别号
private String targetID;//目标客户端识别号
public QClient(Socket socket) {
LOG.info("clinet in "+socket.getInetAddress()+" port "+socket.getLocalPort());
if(sessionSvr==null)sessionSvr=ContextTool.getBean(SessionService.class);//获取会话service
this.socket = socket;
try {
in = socket.getInputStream();
out = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
LOG.info("Client start ");
/**
* 第一步等待客户端发来识别号
*/
if(!isInterrupted() && socket.isConnected() ) {
if (in == null) {
return;//直接返回
}
try {
int len=0;
while(len==0)len= in.available();//等待有数据
LOG.info("Client data len= "+len);
if (len > 0) {
int size = in.read(buffer);//读取数据,第一次链接,就人为读取的是识别码
String id=new String(buffer,0,len);//转换为字符串
LOG.info("client id="+id);
this.clientID=id;//记录客户端id
MySession sess=sessionSvr.getByClientId(id);//查询会话信息
if(sess==null) {
//没有会话
QServer.getInstance(socket.getLocalPort()).removeClient(this);//移除本链接
LOG.info("client no session"+id);
return;//直接结束
}
else {
if(sess.getControlCode().equals(id)) {
this.targetID=sess.getControledCode();//对方是被控端
}
else {
this.targetID=sess.getControlCode();//对方是控制端
}
sess.setStatus(sess.getStatus()+1);
sessionSvr.update(sess);//更新链接
LOG.info("client target id="+this.targetID);
}
if(!clients.containsKey(id)) clients.put(id, this);//存储到集合
}
}
catch (Exception e) {
// TODO: handle exception
return;//直接返回,链接识别,需要重连
}
}
/**
* 其他数据直接转发
*/
while (!isInterrupted() && socket.isConnected() ) {
if (in == null) {
break;
}
try {
int available = in.available();//有数据
if (available > 0) {
int size = in.read(buffer);
if (size > 0) {
//读取到了数据,直接转发给对应的客户端就行
if(clients.containsKey(this.targetID)){
//如果对方上线了
clients.get(this.targetID).sendData(buffer,size);//直接转发过去
}
}
}
} catch (IOException e) {
LOG.error(e.getLocalizedMessage());
e.printStackTrace();
break;
}
try {
Thread.sleep(50);
}
catch (Exception e) {
// TODO: handle exception
}
}
this.remove();//清理资源
}
/**
* 移除这个客户端
*/
public void remove() {
try {
this.interrupt();//中断线程
this.close();//关闭链接
if(this.clientID!=null && clients.containsKey(this.clientID))
clients.remove(this.clientID);//从集合移除
}catch(Exception ex) {
}
}
/**
* 获取地址
* @return
*/
public String getAddress()
{
return socket.getInetAddress().toString();
}
/**
* 发送数据方法
* @param data 数据
* @param len 长度
*/
public void sendData(byte[] data,int len) {
try {
out.write(data,0,len);//发送数据
out.flush();
}
catch (Exception e) {
// TODO: handle exception
//e.printStackTrace();
}
} void close() {
try {
if (in != null) {
in.close();
} if (out != null) {
out.close();
} if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}StartListener类负责在程序启动的时候,完成一些默认的功能,或者设置
**
* 实现启动完毕以后,马上启动一个服务器端
* @author qujia
*
*/
@Component
public class StartListener implements CommandLineRunner { @Autowired
ApplicationContext ctx;//获取spring容器
public void run(String... args) throws Exception {
// TODO Auto-generated method stub
ContextTool.setContext(ctx);//存储到contextTool
QServer.getInstance(7890);//启动默认的服务端口
}}
ContextTool负责给非Spring创建的对象提供手动获取bean的功能,需要在程序启动的时候,设置Context
/***
* 提供applicationcontext.
* 类不是标准的bean,没法自动创建
* @author qujia
*
*/
public class ContextTool implements ApplicationContextAware, DisposableBean {
private static ApplicationContext applicationContext =null;
/**
* 获取applicationContext
*/
public static ApplicationContext getApplicationContext() {
if (applicationContext == null) {
throw new IllegalStateException("applicaitonContext属性未注入, 请在SpringBoot启动类中注册SpringContextHolder.");
}else {
return applicationContext;
}
}
/**
* 设备context
* @param ctx
*/
public static void setContext(ApplicationContext ctx) {
applicationContext=ctx;
}
/**
* 通过name获取 Bean
* @param name 类名称
* @return 实例对象
*/
public static Object getBean(String name){
return getApplicationContext().getBean(name);
} /**
* 通过class获取Bean
*/
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
} /**
* 通过name,以及Clazz返回指定的Bean
*/
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
} @Override
public void destroy() {
applicationContext = null;
} @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
applicationContext=applicationContext;
System.out.print(" context set "+applicationContext.getClass());
}}
------------------------------------------------------------------------------------------------
测试结果:
链接成功后,直接发送11111,由于没有这个会话,链接直接断开
数据创建一个123到321的会话,两个客户端链接成功,并且可以相互发消息