现在谈谈自己对TCP长连接的一些粗浅见解。
1.首先,使用TCP发送信息,其底层也是将信息拆分成若干报文进行发送,在到达目的地按发送的先后顺序重新组装起来。
其实,相对的每个报文都是有超时限制的,当然,当你不发送报文时,空闲也有超时限制。表现在java里,就是会有异常抛出。
2.其次,为了保持在无有效数据的交互情况下连接不会超时断开,我们从程序上,会人为的发送一些特殊的只用于维持连接不断的信息包。
这些包,有的人称之为心跳包。我理解的心跳一词,貌似应该是用于信息同步的,当然,用于维持连接,我就不知道是否正确了。
反正,暂且先这么理解吧。
3.既然,所发信息已经产生了分类的情况(最起码,心跳包就可归为一类),那么,势必牵扯到应用层协议的拟定了。
也就是说,我们收发双方,要按照共同制定的信息格式进行收发信息,这样,才能相互理解所发的内容。
4.一般情况下,应用层协议,也会参照OSI的设计逻辑,将每个发送的信息分为包头和包体两个部分。
为了简化解析难度,包头一般都是定长的,内容类型也都会有固定的格式。比如按照:包长、类型、序列号 这样的顺序一次发送。
包体依据包头当中的类型字段的不同,而采用不同的格式记录信息。心跳包,一般包体为空,不必填充。
5.网络变成,历来都是考验程序员综合能力的一种挑战,尤其是针对初学者。对于多线程,网络连接,数据的协同处理,等等,
必须要有相当的逻辑思维能力,与全局关。当然,我这方面还是不行,所以,代码也比较垃圾。
本例中,为了完成心跳包的定时发送,其他代码不写的我都没有写,包头的序列号部分我也没写(写了这个,处理起来就比较麻烦了)。
好了,现在开始秀代码,希望更多的朋友能够提出一些好的见解,大家共同进步,共同成长。
一、先介绍两个工具类。用于基本数据类型和字节数组的相互转换。
package houlei.net.keepconn.utils;
/**
* 字节流缓冲区,辅助完成对象向字节流的转换。
* <p>
* 创建时间:2009-10-28 下午02:59:51
* @author 侯磊
* @since 1.0
*/
public class ByteArrayBuilder {
private int count=0;
private byte [] buf = null;
public ByteArrayBuilder(){
buf = new byte[8];
}
public ByteArrayBuilder(int capacity){
buf = new byte[capacity];
}
void expandCapacity(int minimumCapacity) {
int newCapacity = (buf.length + 1) * 2;
if (newCapacity < 0) {
newCapacity = Integer.MAX_VALUE;
} else if (minimumCapacity > newCapacity) {
newCapacity = minimumCapacity;
}
byte newBuff[] = new byte[newCapacity];
System.arraycopy(buf, 0, newBuff, 0, count);
buf = newBuff;
}
public ByteArrayBuilder write(int i) {
int newcount = count + 4;
if (newcount > buf.length) {
expandCapacity(newcount);
}
buf[count+3]=(byte)(i&0x000000FF);
buf[count+2]=(byte)((i&0x0000FF00)>>8);
buf[count+1]=(byte)((i&0x00FF0000)>>16);
buf[count] = (byte)((i&0xFF000000)>>24);
count = newcount;
return this;
}
public ByteArrayBuilder write(byte [] b, int off, int len) {
if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return this;
}
int newcount = count + len;
if (newcount > buf.length) {
expandCapacity(newcount);
}
System.arraycopy(b, off, buf, count, len);
count = newcount;
return this;
}
public ByteArrayBuilder write(byte [] b){
return write(b,0,b.length);
}
public byte [] toBytes(){
if(count==buf.length) return buf;
byte newbuff [] = new byte[count];
System.arraycopy(buf, 0, newbuff, 0, count);
return newbuff;
}
}
package houlei.net.keepconn.utils;
/**
* 可以从字节数组中读取所需要的数据
* <p>
* 创建时间:2009-10-28 下午03:36:48
* @author 侯磊
* @since 1.0
*/
public class ByteArrayReader {
private int point=0;
private byte [] buf = null;
public ByteArrayReader(byte [] b){
buf=b;
}
public int readInt(int offset){
int i = buf[offset++];
i=((i<<8)|buf[offset++]);
i=((i<<8)|buf[offset++]);
i=((i<<8)|buf[offset++]);
return i;
}
public byte readByte(int offset){
return buf[offset];
}
public int readInt(){
int i = buf[point++];
i=((i<<8)|buf[point++]);
i=((i<<8)|buf[point++]);
i=((i<<8)|buf[point++]);
return i;
}
public byte readByte(){
return readByte(point++);
}
}
二、介绍几个数据包的封装类。
package houlei.net.keepconn.messages;
/**
* 网络数据包对应类的最高抽象<br/>
* 一般数据包分包头和包体两个部分,包头一般含包长度和包类型等信息,包体是本包所承载的内容。<br/>
* 本例中,包头就包含长度和类型两个信息,分别占4字节空间。
* <p>
* 创建时间:2009-10-28 下午02:12:43
* @author 侯磊
* @since 1.0
*/
public interface Message {
/**
* 网络数据包类型:心跳包(请求包)
*/
public static final int ActiveTestRequest = 0x00000001;
/**
* 网络数据包类型:心跳包(应答包)
*/
public static final int ActiveTestResponse = 0x80000001;
/**
* 网络数据包类型:登陆包(请求包)
*/
public static final int LoginRequest = 0x00000002;
/**
* 网络数据包类型:登陆包(应答包)
*/
public static final int LoginResponse = 0x80000002;
/**
* 网络数据包类型:登出包(请求包)
*/
public static final int LogoutRequest = 0x00000003;
/**
* 网络数据包类型:登出包(应答包)
*/
public static final int LogoutResponse = 0x80000003;
/*
* 这里还可以添加其他信息包的类型
* */
/**
* 获取数据包的总长度(包括包头加包体的长度)
* @return数据包的总长度
*/
public abstract int getMessageLength();
/**
* 获取数据包的类型
* @return数据包的类型
*/
public abstract int getMessageType();
/**
* 解析数据包的内容。使字节流转换成类对象。该方法主要完成对包体的解析过程。
* @param b 字节数组(字节流)
* @throws ParseException 当解析时发生异常时抛出
*/
public abstract void parse(byte [] b) throws ParseException;
/**
* 将类对象的内容转换成字节数组
* @return 类对象对应的字节数组
*/
public abstract byte [] getBytes();
}
package houlei.net.keepconn.messages;
/**
* 网络数据包对应类的抽象类
* <p>
* 创建时间:2009-10-28 下午02:47:15
* @author 侯磊
* @since 1.0
*/
public abstract class AbstractMessage {
/**
* 包头长度。(协议采用定长包头,长度为8)
*/
public static final int MessageHeaderLength = 8;
}package houlei.net.keepconn.messages;
import houlei.net.keepconn.utils.ByteArrayBuilder;
/**
* 心跳包(请求包)
* <p>
* 创建时间:2009-10-28 下午02:48:36
* @author 侯磊
* @since 1.0
*/
public class ActiveTestRequest extends AbstractMessage implements Message {
/* (非 Javadoc)
* @see houlei.net.keepconn.messages.Message#getBytes()
*/
public byte[] getBytes() {
return new ByteArrayBuilder().write(MessageHeaderLength).write(ActiveTestRequest).toBytes();
}
/* (非 Javadoc)
* @see houlei.net.keepconn.messages.Message#getMessageLength()
*/
public int getMessageLength() {
return MessageHeaderLength;
}
/* (非 Javadoc)
* @see houlei.net.keepconn.messages.Message#getMessageType()
*/
public int getMessageType() {
return ActiveTestRequest;
}
/* (非 Javadoc)
* @see houlei.net.keepconn.messages.Message#parse(byte[])
*/
public void parse(byte[] b) throws ParseException {
//空包体,所以空函数体。
}
}
package houlei.net.keepconn.messages;
import houlei.net.keepconn.utils.ByteArrayBuilder;
/**
*心跳包(应答包)
* <p>
* 创建时间:2009-10-28 下午04:11:04
* @author 侯磊
* @since 1.0
*/
public class ActiveTestResponse extends AbstractMessage implements Message {
/* (非 Javadoc)
* @see houlei.net.keepconn.messages.Message#getBytes()
*/
public byte[] getBytes() {
return new ByteArrayBuilder().write(MessageHeaderLength).write(ActiveTestResponse).toBytes();
}
/* (非 Javadoc)
* @see houlei.net.keepconn.messages.Message#getMessageLength()
*/
public int getMessageLength() {
return MessageHeaderLength;
}
/* (非 Javadoc)
* @see houlei.net.keepconn.messages.Message#getMessageType()
*/
public int getMessageType() {
return ActiveTestResponse;
}
/* (非 Javadoc)
* @see houlei.net.keepconn.messages.Message#parse(byte[])
*/
public void parse(byte[] b) throws ParseException {
// 空包体,所以空函数体。
}
}
package houlei.net.keepconn.messages;
/**
*
* <p>
* 创建时间:2009-10-28 下午04:37:53
* @author 侯磊
* @since 1.0
*/
public class MessageFactory {
public static Message getInstance(int messageType){
Message m = null;
switch(messageType){
case Message.ActiveTestRequest : m= new ActiveTestRequest();break;
case Message.ActiveTestResponse : m= new ActiveTestResponse();break;
default:throw new RuntimeException("所需求的数据包类未能提供");
}
return m;
}
}package houlei.net.keepconn.messages;
/**
* 解析数据包产生异常时抛出。
* <p>
* 创建时间:2009-10-28 下午02:42:16
* @author 侯磊
* @since 1.0
*/
public class ParseException extends Exception {
private static final long serialVersionUID = 1L;
public ParseException() {
}
public ParseException(String message) {
super(message);
}
public ParseException(Throwable cause) {
super(cause);
}
public ParseException(String message, Throwable cause) {
super(message, cause);
}
}
三、客户端长连接的封装类
package houlei.net.keepconn.client;
import houlei.net.keepconn.messages.AbstractMessage;
import houlei.net.keepconn.messages.Message;
import houlei.net.keepconn.messages.MessageFactory;
import houlei.net.keepconn.messages.ParseException;
import houlei.net.keepconn.utils.ByteArrayReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* 客户端长连接的封装类。
* <p>
* 创建时间:2009-10-28 下午04:17:34
* @author 侯磊
* @since 1.0
*/
public class Connection {
private Socket socket;
private OutputStream out;
private InputStream in ;
private long lastActTime = 0;
Connection(String host,int port) throws IOException{
socket = new Socket();
socket.connect(new InetSocketAddress(host,port));
in =socket.getInputStream();
out = socket.getOutputStream();
}
Connection(Socket socket) throws IOException{
this.socket=socket;
in =socket.getInputStream();
out = socket.getOutputStream();
}
private void send0(Message m) throws IOException{
lastActTime = System.currentTimeMillis();
out.write(m.getBytes());
out.flush();
}
private Message readWithBlock0() throws IOException, ParseException{
lastActTime = System.currentTimeMillis();
byte [] header = new byte[AbstractMessage.MessageHeaderLength];
if(in.read(header)!=AbstractMessage.MessageHeaderLength)
throw new IOException("未能读取完整的包头部分");
ByteArrayReader bar = new ByteArrayReader(header);
int len = bar.readInt();
if(len<0)throw new ParseException("错误的包长度信息");
int type = bar.readInt();
byte [] cache = new byte [len];
System.arraycopy(header, 0, cache, 0, header.length);
if(in.read(cache, header.length, len-header.length)!=len-header.length)
throw new IOException("未能读取完整的包体部分");
Message m = MessageFactory.getInstance(type);
m.parse(cache);
return m;
}
/**
* 用于发送数据包,由于包头缺少序列号,所以,交互过程,每一阶段必须等到上一阶段应答包收到才能发起下次的请求包。
* @param m 待发送的信息包
* @return 对应的应答包
* @throws IOException
* @throws ParseException
*/
public synchronized Message send(Message m) throws IOException, ParseException{
send0(m);
return readWithBlock0();
}
public synchronized void close() throws IOException{
lastActTime = System.currentTimeMillis();
ConnectionManager.removeConnection(this);
if(socket!=null)socket.close();
if(in!=null)in.close();
if(out!=null)out.close();
}
public synchronized long getLastActTime(){
return lastActTime;
}
}
package houlei.net.keepconn.client;
import houlei.net.keepconn.messages.Message;
import houlei.net.keepconn.messages.MessageFactory;
import houlei.net.keepconn.messages.ParseException;
import java.io.IOException;
import java.net.Socket;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* 客户端长连接管理类,负责维持所产生的长连接。
* <p>
* 创建时间:2009-10-28 下午04:17:09
* @author 侯磊
* @since 1.0
*/
public class ConnectionManager {
/**
* 心跳周期(单位:毫秒)
*/
private volatile static long activeCycle = 1000;
/**
* 存放产生的长连接
*/
private static Set<Connection> pool = Collections.synchronizedSet(new HashSet<Connection>());
/**
* 用于定时发送心跳包
*/
private static ConnectActiveMonitor monitor = new ConnectActiveMonitor();
static{
monitor.start();
}
public static Connection createConnection(String host,int port) throws IOException{
Connection conn = new Connection(host,port);
pool.add(conn);
return conn;
}
public static Connection createConnection(Socket socket) throws IOException{
Connection conn = new Connection(socket);
pool.add(conn);
return conn;
}
public static void removeConnection(Connection conn){
pool.remove(conn);
}
static class ConnectActiveMonitor extends Thread{
private volatile boolean running = true;
public void run(){
while(running){
long time = System.currentTimeMillis();
for(Connection con : pool){
try {
if(con.getLastActTime()+activeCycle<time)
con.send(MessageFactory.getInstance(Message.ActiveTestRequest));
} catch (IOException e) {
removeConnection(con);
} catch (ParseException e) {
}
}
yield();
}
}
void setRunning(boolean b){
running = b;
}
}
}
四,最后就是测试心跳包的简单代码了
package houlei.net.keepconn.test;
import houlei.net.keepconn.client.ConnectionManager;
import java.io.IOException;
/**
* 测试心跳包的简单客户端程序
* <p>
* 创建时间:2009-10-28 下午05:28:10
*
* @author 侯磊
* @since 1.0
*/
public class TestClient {
public static void main(String[] args) throws IOException {
ConnectionManager.createConnection("localhost", 65432);
}
}
package houlei.net.keepconn.test;
import houlei.net.keepconn.messages.AbstractMessage;
import houlei.net.keepconn.messages.Message;
import houlei.net.keepconn.messages.MessageFactory;
import houlei.net.keepconn.messages.ParseException;
import houlei.net.keepconn.utils.ByteArrayReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 测试心跳包的服务端简单程序
* <p>
* 创建时间:2009-10-28 下午05:28:20
*
* @author 侯磊
* @since 1.0
*/
public class TestServer {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(65432);
Socket s = ss.accept();
new SimpleProcessor(s).start();
}
static class SimpleProcessor extends Thread{
private Socket socket;
private OutputStream out;
private InputStream in ;
private volatile boolean running = true;
public SimpleProcessor(Socket s) throws IOException {
this.socket = s;
in = s.getInputStream();
out = s.getOutputStream();
}
public void run(){
while(running){
try {
Message m = read();
System.out.println("收到信息");
if(m.getMessageType()==Message.ActiveTestRequest){
m = MessageFactory.getInstance(Message.ActiveTestResponse);
}
send(m);
} catch (Exception e) {
e.printStackTrace();
}
}
try {
if(socket!=null)socket.close();
} catch (IOException e) {
}
}
private void send(Message m) throws IOException{
out.write(m.getBytes());
out.flush();
}
private Message read() throws IOException, ParseException{
byte [] header = new byte[AbstractMessage.MessageHeaderLength];
if(in.read(header)!=AbstractMessage.MessageHeaderLength)
throw new IOException("未能读取完整的包头部分");
ByteArrayReader bar = new ByteArrayReader(header);
int len = bar.readInt();
if(len<0)throw new ParseException("错误的包长度信息");
int type = bar.readInt();
byte [] cache = new byte [len];
System.arraycopy(header, 0, cache, 0, header.length);
if(in.read(cache, header.length, len-header.length)!=len-header.length)
throw new IOException("未能读取完整的包体部分");
Message m = MessageFactory.getInstance(type);
m.parse(cache);
return m;
}
}
}