Java服务器与客户端通信框架初探

这篇文章中,我们讲述一下用Java实现网络通信的的基本流程,这里讲述的是不基于任何框架的原生语言自带的写法。

Java服务器端写法:

程序入口代码如下:

public static void main(String[] args) {
	try {
		// ① 创建一个线程 等其他客户端的连接
		final ServerSocket server = new ServerSocket(8090);
		System.out.println("---服务器启动---" + new Date().toString());
		new Thread() {//
			public void run() {
				while (true) {
					QQConnection conn = null;
					try {
						Socket client = server.accept();
						System.out.println("---有客户端接入---" + client);
						// ② 如果客户端连接成功分配置一个线程
						conn = new QQConnection(client);
						conn.addOnRecevieMsgListener(new LoginMsgListener(conn));
						conn.addOnRecevieMsgListener(new ChatP2PListener());
						conn.addOnRecevieMsgListener(new ChatRoomListener());
						conn.addOnRecevieMsgListener(new LoginOutListener());
						// ③ 该线程内等待用户数据
						conn.connect();
						// ④ 分配一个线程给客户端
					} catch (IOException e) {
						e.printStackTrace();
						conn.disconnect();
					}
				}
			};
		}.start();

	} catch (Exception e) {//
		e.printStackTrace();
	}
}



上面的代码大致可以分为以下几个步骤:

1. 使用Java.net.ServerSocket指定一个端口,创建一个服务器侦听的Socket。

ServerSocket server = new ServerSocket(8090);

2.开启一个线程,在这个线程里面创建一个循环,接收客户端的连接,每有一个客户端连接上来后产生一个连接对象(QQConnection)。

conn = new QQConnection(client);

public QQConnection(Socket socket) {
	super();
	try {
		this.socket = socket;
		writer = new DataOutputStream(this.socket.getOutputStream());
		reader = new DataInputStream(this.socket.getInputStream());
	} catch (Exception e) {
		e.printStackTrace();
	}
}

在创建QQConnection的构造函数里面,同时得到这路连接的发送缓冲区和接收缓冲区对应的流对象,将来客户端有数据来的话,从reader对象中读取,发给客户端数据通过writer对象:

public class QQConnection extends Thread {
	//java.io.DataOutputStream
	public DataOutputStream writer = null;
	//java.io.DataInputStream
	public DataInputStream reader = null;
}


3. 每一个客户度连接开一个线程进行处理(这种处理方式不好,但是我们这里只是讨论大致流程):

conn.connect();

//QQConnection.connect()方法
public void connect() {
	flag = true;
	//QQConnection继承自java.lang.Thread
	start();
}

开启的线程函数如下:

@Override
public void run() {
	super.run();
	// 等待 数据
	while (flag) {
		try {
			String xml = reader.readUTF();
			System.out.println(xml);
			if (xml != null && !"".equals(xml)) {
				QQMessage msg = new QQMessage();
				msg = (QQMessage) msg.fromXml(xml);
				for (OnRecevieMsgListener l : listeners) {
					l.onReceive(msg);
				}
			}
		} catch (EOFException e) {
//				e.printStackTrace();
			System.out.println("=-=EOFException---");
			if (who != null) {
				QQConnectionManager.remove(who.account);
			}
			disconnect();
		} catch (Exception e) {
			e.printStackTrace();
			if (who != null) {
				QQConnectionManager.remove(who.account);
			}
			disconnect();
		}
	}
}

4. 线程函数里面先接收数据,再将接收好的数据送给每个需要处理的类,这些类分别是:
LoginMsgListener、ChatP2PListener、ChatRoomListener、LoginOutListener。这些类都是通过多态的机制保存在conn对象的一个集合中(java.util.ArrayList)。
private List<OnRecevieMsgListener> listeners = new ArrayList<OnRecevieMsgListener>();

public static interface OnRecevieMsgListener {
	public void onReceive(QQMessage msg);
}

public class LoginMsgListener extends MessageSender implements OnRecevieMsgListener {
}

public class ChatP2PListener extends MessageSender implements OnRecevieMsgListener {
}

public class ChatRoomListener extends MessageSender implements OnRecevieMsgListener {
}

public class LoginOutListener extends MessageSender implements OnRecevieMsgListener {
}

这些类的对象在接受新连接时放入连接对象的ArrayList中:

conn.addOnRecevieMsgListener(new LoginMsgListener(conn));
conn.addOnRecevieMsgListener(new ChatP2PListener());
conn.addOnRecevieMsgListener(new ChatRoomListener());
conn.addOnRecevieMsgListener(new LoginOutListener());

这样,等数据来的时候,将数据收到后,遍历这个ArrayList进行数据处理:

String xml = reader.readUTF();
System.out.println(xml);
if (xml != null && !"".equals(xml)) {
	QQMessage msg = new QQMessage();
	msg = (QQMessage) msg.fromXml(xml);
	for (OnRecevieMsgListener l : listeners) {
		l.onReceive(msg);
	}
}

根据多态在子类的OnRecv函数里面处理完数据之后,如何将应答结果发给客户端呢?
1. 首先,这些子类都继承了一个公共类MessageSender,这个类实现了应答了数据的流程。当然无论是你是采取接口改写还是抽象类方法改写甚至像下面这样直接共有方法调用都是可以的(当然这里所有的连接对象都保存在一个QQConnectionManager的成员变量conns中,这是一个java.util.HashMap对象):

public class QQConnectionManager {
	public static HashMap<Long, QQConnection> conns = new HashMap<Long, QQConnection>();
}

public class MessageSender {
	/**
	 * 给一个客户端发送消息,点对点聊天
	 * 
	 * @param msg
	 * @param conn
	 * @throws IOException
	 */
	public void toClient(QQMessage msg, QQConnection conn) throws IOException {
		System.out.println("单发当前客户端to Client \n" + msg.toXml());
		if (conn != null) {
			conn.writer.writeUTF(msg.toXml());
		}
	}

	/**
	 * 给连接进来的所有的客户端发送消息
	 * 
	 * @param msg
	 * @throws IOException
	 */
	public void toEveryClient(QQMessage msg) throws IOException {
		System.out.println("群发所有客户端  to toEveryClient Client \n" + msg.toXml());
		// conn.writer.writeUTF(toClient.toXml());
		Set<Map.Entry<Long, QQConnection>> allOnLines = QQConnectionManager.conns
				.entrySet();
		for (Map.Entry<Long, QQConnection> entry : allOnLines) {
			entry.getValue().writer.writeUTF(msg.toXml());
		}
	}

	public void toOtherClient(QQMessage msg) throws IOException {
		System.out.println("群发所有其他客户端  to toEveryClient Client \n"
				+ msg.toXml());
		// conn.writer.writeUTF(toClient.toXml());
		Set<Map.Entry<Long, QQConnection>> allOnLines = QQConnectionManager.conns
				.entrySet();
		for (Map.Entry<Long, QQConnection> entry : allOnLines) {
			if (entry.getValue().who.account != msg.from) {
				entry.getValue().writer.writeUTF(msg.toXml());
			}
		}
	}
}

客户端写法:

客户端我们以安卓代码为例:

透过现象看本质,再复杂的客户度程序无非需要解决如下两个问题:

问题一:客户端产生网络调用的一般是UI线程,但是一般不直接在UI线程进行网络数据,所以要单独开一个或者多个网络线程,这就涉及到UI线程如何把数据传给网络线程。

问题二:当网络线程收到数据后,或者发送数据(成功或失败)如何反馈给UI线程,这就涉及到网络线程如何和UI线程通信。


无论是在程序启动时还是在实际需要联网时再启动网络线程都可以,比如这里我们在登录界面初始化时,连接服务器,如果连接成功,则创建一个Connection对象:

public class LoginActivity extends Activity {	
	QQConnection conn;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_login);		
		/*
		 * 登录的逻辑:首先连接服务器,获取用户输入的账号和密码,讲账户和密码发送给服务器;服务器做一些验证操作,将验证结果返回给客户端,
		 * 客户端再进行接收消息的操作
		 */
		// 与网络相关的操作要放在子线程中进行
		ThreadUtils.runInSubThread(new Runnable() {

			public void run() {
				try {
					conn = new QQConnection("192.168.0.148", 8090);// Socket
					conn.connect();// 建立连接
					// 建立连接之后,将监听器添加到连接里面
					conn.addOnMessageListener(listener);
				} catch (Exception e) {
					e.printStackTrace();
				}

			}
		});

	}
}

如果连接成功后,则初始化输入输出流:

public class QQConnection {
	private DataInputStream reader;
	private DataOutputStream writer;


	/**
	 * new出QQConnection对象的时候初始化IP地址和端口
	 * 
	 * @param host
	 * @param port
	 */
	public QQConnection(String host, int port) {
		super();
		this.host = host;
		this.port = port;
	}

	/**
	 * 创建与服务器之间的连接
	 * 
	 * @throws IOException
	 * @throws UnknownHostException
	 */
	public void connect() throws UnknownHostException, IOException {
		// 创建连接
		client = new Socket(host, port);
		reader = new DataInputStream(client.getInputStream());
		writer = new DataOutputStream(client.getOutputStream());
	}
}


同样,这两个流对象reader和writer我们将来用户和服务器进行通信,利用reader收取服务器发来的数据,利用writer发送数据给服务器。


我们现在以登录为例:

网络线程继承android的android.os.HandlerThread,这个对象里面我们创建一个Handler对象,这个对象可以在被UI线程访问:

在UI线程里面调用:

Message msg = new Message();
msg.what = MessageType.MSG_TYPE_LOGIN;
//携带登录参数(比如用户名、密码、客户端类型等)
msg.obj = LoginInfo;
//mNetWorkHandler是网络线程对象的handler成员
NetworkHandlerThread.mNetworkHandler.sendMessage(msg);

然后在网络线程里面:

public class NetworkHandlerThread extend HandlerThread{
	static Handler NetworkHandlerThread = new Handler(){
		@Override
		public void handleMessage(Message msg) {
			if (msg.what == MessageType.MSG_TYPE_LOGIN){
				//发送登录数据包
				//如果发送失败,向界面反馈
			} else if (msg.what == MessageType.MSG_TYPE_TYPE2)
			{
				//其它类型
			}			
		}
	};
}

发送数据给服务器其实就是使用连接对象的writer,比如:

write.writeInt(cmd);
write.writeUTF8(username);
write.writeUTF8(password);

那发送成功或者发送失败,或者收到网络数据如何反馈给UI线程呢?同样的道理,在安卓的UI界面元素,比如LoginActivity,也创建一个android Handler对象,网络线程通过Handler.sendMessage()方法发给UI线程处理。代码和上面基本类似,这里就不列举了。

这里列举了一种最常用的UI线程与网络线程交互方式。当然,在安卓平台上你可以将网络线程用其它支线程的形式来处理,比如消息队列(例java.util.BlockQueue)或者AsyncTask等。


总结:

我本人日常工作主要以C++为主,偶尔会写写java或者安卓代码。与C++网络通信代码来说的话,java代替你做了很多重复的工作,以使你集中精力在业务代码上面,这些重复代码主要有以下几点:

1. 无论是服务器创建侦听socket还是客户端连接服务器socket,在C++代码中你必须先创建socket,然后调用bind和listen方法去启动侦听(对于服务器),或者调用connect去连接服务器(对于客户端);而Java代码直接以一个java.net.ServerSocket中指定端口号和java.net.Socket指定服务器地址和端口号来简化了,而jdk库帮你做了C++那些底层的重复工作。

2. 发送数据和接收数据,在C++代码中,你一般需要自己维护一份发送缓冲区和接收缓冲区用来数据的发送和接收,而Java则直接将一个socket对象中抽象出来发送流和接收流,所以的数据直接从流上面读取和发送就行了,而你需要什么格式的数据,你可以直接使用readInt/writeInt、readUTF8/writeUTF8等确定的类型从流中读取或者写入。而对于C++你发送和接收的都是字节流,你必须手工解析出你想要的类型(当然java的流也提供了接收和发送字节数组的接口)。不得不说java封装的真是方便啊。很多年以前,我在学习flash平台时,它的网络api也提供类似readInt/writeInt、readUTF8/writeUTF8这样的接口,我当时真是一头雾水,真的不知道这些接口有啥用,比如读取一个int型,到底是怎么得到,当写了很多年的C++网络通信程序并自己封装了一些网络框架后,回过头来看这些接口真是恍然大悟啊。


      这篇文章,其实我是作为自己的java学习笔记的,目的是帮助自己记忆java网络通信的一些细节和梳理java网络通信框架一些原理性的东西。所以这里就不提供代码了,示例中的代码源自http://blog.csdn.net/qq_20889581/article/details/50755449的开源项目,在这里感谢原作者。





此基于springmvc框架,是服务器之间的交互框架. 分服务端客户端. 没有什么背景,就是自己按自己想法随心写的一个...也没经过大量的测试,可以供小白参考参考.欢迎各位提出点改进意见... 使用: 1.引入此maven项目 2.服务端mvc增加配置 <bean class="com.osc.controller.OscMainController"></bean> 3.客户端mvc增加配置 <bean class="com.osc.processor.KingBeanScannerConfigurer"> // 这是接口存放的包..所有的接口都放此包下面 <property name="basePackage" value="com.eat.conInter"></property> <property name="urlMap"> <map> // 这里是配置服务端的地址.多个地址以次写下去...此处是示范啊.. <entry key="url1" value="http://192.168.3.113:8081/eat-app" /> <entry key="url2" value="PersonBean" /> </map> </property> </bean> 4.基本引入完毕..使用规则: 1.新建接口 [示范] ps:此接口要放入上面配置的包 com.eat.conInter 包下面 @Service("IExample") @IsConn(url=url1) //当有多个服务器的地址时此处须配置url=url1 就是上面配置的名字 ,默认是第一个地址.. public interface IExample { public Stu getName(String a,int b,Stu su); } 2 服务端实现接口 @Service("IExampleImpl") // 实现接口的类名 接口名+Impl @Invoke //ps:此注解是标识..无此注解将不能调用的类 public class IExampleImpl implements IExample{ public Stu getName(String a, int b, Stu su) { System.out.println("这是Impl.getName"); return new Stu(); } } 5.客户端调用 使用springmvc的注解注入 @Autowired private @Qualifier("IExample")IExample iExample; 然后可直接在方法里面调用 iExample.getName("",4,new Stu()); 编译执行,可以看到控制台打印这是 Impl.getName
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值