现在谈谈自己对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;
		}
	}
}