不过也可以通过UDP打洞的方式来实现穿透NAT,QQ就是用UDP打洞的方式,不过还有先进的方法保证UDP的可靠性。但是UDP打洞也是很难实现的。
经过半天的思考,还是决定用一种简单的方法实现 客户端-服务器-客户端 的通信,基于Socket,在服务端保存一张Map,map键值分别为username,socket。
再通过服务器的并发实现客户端间的通信,客户端发送的所有数据都先传到服务器,服务器再把数据解析发送到对应的接收端。
架构是这样的:
客户端连接服务器,服务端start一个服务线程,用户输入自己的名字,发送给服务器端,服务器将username和当前连接的socket保存到一张hashmap中。
如果已经有用户使用了该名字,服务器查询map中已经有这个用户吗,有的话通知用户重新输入用户名。
确定username后,再选择要发送信息的好友,服务器再从map中拿出好友的socket,再把消息转发至好友的socket
客户端接收方面,启用一个后台进程,去read自己的socket获取消息。
更重要的是,后台线程和客户端主线程间用一个单向的管道,传送一些必要的信息。
整个架构中都没有忙等待,都是通过阻塞的方式进行数据的传递。所以在性能上是很强的。
由于只是为了实现这个功能,代码写好后就没有改过,代码还是挺粗糙的,经过测试,剩下一个Bug就是linux端和windows端进行中文通信的时候就会有乱码,这个就不去处理了。
附源码:
SmallQQClient.zip
SmallQQServer.zip给出服务器端代码:
QQServer.java
-
package cn.com.xiebiao.smallQQServer;
-
-
import java.io.IOException;
-
import java.net.ServerSocket;
-
import java.net.Socket;
-
-
/**
-
*
- * Title : QQServer.java
-
* Author : Vibe Xie @
-
* Time : Mar 22, 2015 3:24:07 PM
-
* Copyright: Copyright (c) 2015
-
* Description:
-
*/
-
-
public class QQServer {
-
private static ServerSocket serverSocket=null;
-
private static Socket socket=null;
-
//服务器端口
-
private static int SERVER_PORT=8999;
-
//服务次数
-
private static int SERVER_TIMES=1;
-
-
public static void main(String[] args) {
-
// TODO Auto-generated method stub
-
try {
-
serverSocket=new ServerSocket(SERVER_PORT);
-
System.out.println("QQ服务器启动...");
-
while(true){
-
socket=serverSocket.accept();
-
System.out.println("服务器第"+(SERVER_TIMES++)+"次连接");
-
new Thread(new ServerThread(socket)).start();
-
}
-
-
} catch (IOException e) {
-
// TODO Auto-generated catch block
-
e.printStackTrace();
-
}finally{
-
try {
-
serverSocket.close();
-
} catch (IOException e) {
-
// TODO Auto-generated catch block
-
e.printStackTrace();
-
}
-
}
-
-
}
- }
ServerThread.java
-
package cn.com.xiebiao.smallQQServer;
-
-
import java.io.BufferedInputStream;
-
import java.io.BufferedReader;
-
import java.io.BufferedWriter;
-
import java.io.IOException;
-
import java.io.InputStreamReader;
-
import java.io.OutputStreamWriter;
-
import java.net.Socket;
-
import java.util.Iterator;
-
import java.util.Map;
-
import java.util.Set;
-
-
/**
-
*
- * Title : ClientThread.java
-
* Author : Vibe Xie @
-
* Time : Mar 21, 2015 2:34:04 PM
-
* Copyright: Copyright (c) 2015
-
* Description: ClientThread接收发送端发来的消息,再把消息通过WriteThead线程发送给接收端
-
*/
-
public class ServerThread implements Runnable {
-
private Socket socket;
-
private boolean flag=true;
-
//msg的格式是 "sender>receiver>"+正文
-
private String msg=null;
-
private MsgAnalyseUtil msgAnalyse;
-
//将msg拆解为sender,receiver,message
-
private String sender;
-
private String receiver;
-
private String message;
-
-
private BufferedReader bufferedReader;
-
private BufferedWriter writer;
-
public ServerThread(Socket socket) {
-
// TODO Auto-generated constructor stub
-
this.socket=socket;
-
}
-
-
@Override
-
public void run() {
-
// TODO Auto-generated method stub
-
try {
-
-
/******************登录验证模块,用户名已存在,则放回fail,否则返回success************************/
-
bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
-
writer=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
-
//连接成功则从socket读取用户名
-
sender=bufferedReader.readLine();
-
//用户已存在,返回登录失败信息
-
while(UserTable.isUserExist(sender)==true){
-
writer.write("fail");
-
writer.newLine();
-
writer.flush();
-
sender=bufferedReader.readLine();
-
}
-
-
//将用户名和socket保存到用户表中
-
UserTable.userAdd(sender, socket);
-
//收到的用户名不存在,返回登录成功
-
writer.write("success");
-
writer.newLine();
-
writer.flush();
-
/******************登录验证模块*************************************************************/
-
-
while(flag==true){
-
msg=bufferedReader.readLine();
-
//进行拆解
-
System.out.println("服务器接收"+msg);
-
msgAnalyse=new MsgAnalyseUtil(msg);
-
sender=msgAnalyse.getSender();
-
receiver=msgAnalyse.getReceiver();
-
message=msgAnalyse.getMessage();
-
-
//接收到验证好友是否在线指令
-
if(sender.equals("/instruction")){
-
//在UserTable表中查询好友是否在线,并重新包装消息,返回给请求者
-
if(UserTable.isUserExist(receiver)){
-
message="y";
-
}else {
-
message="n";
-
}
-
//包装消息
-
msg=sender+">"+"tmp>"+message;
-
writer.write(msg);
-
writer.newLine();
-
writer.flush();
-
}else{
-
//未接收到指令,进行消息转发
-
//服务器输出消息
-
System.out.println("解析为 发送者:"+sender+" 接收者:"+receiver+" 信息:"+message);
-
-
if(message.equals("bye")){
-
System.out.println("用户退出");
-
-
//退出是通知客户端后台线程结束
-
msg="/instruction"+">"+"tmp>"+"bye";
-
writer.write(msg);
-
writer.newLine();
-
writer.flush();
-
-
//从UserTable中删除该用户
-
UserTable.deleteUser(sender);
-
-
//结束服务器线程
-
flag=false;
-
}else {
-
//转发消息给接收者
-
writer=new BufferedWriter(new OutputStreamWriter(UserTable.getSocket(receiver).getOutputStream()));
-
writer.write(msg);
-
writer.newLine();
-
writer.flush();
-
}
-
}
-
}
-
} catch (Exception e) {
-
// TODO: handle exception
-
e.printStackTrace();
-
}
-
-
}
- }
UserTable.java
-
package cn.com.xiebiao.smallQQServer;
-
-
import java.net.Socket;
-
import java.util.HashMap;
-
import java.util.Map;
-
/**
-
*
- * Title : UserTable.java
-
* Author : Vibe Xie @
-
* Time : Mar 22, 2015 3:25:35 PM
-
* Copyright: Copyright (c) 2015
-
* Description:
-
*/
-
class UserTable{
-
private static Map<String, Socket> userTable=new HashMap<String, Socket>();
-
-
public static void userAdd(String user,Socket socket){
-
userTable.put(user,socket);
-
}
-
-
public static void deleteUser(String user){
-
for(Map.Entry<String,Socket> entry:UserTable.returnUserTable().entrySet()){
-
if(entry.getKey().equals(user)){
-
userTable.remove(entry.getKey());
-
}
-
}
-
}
-
-
public static boolean isUserExist(String user){
-
boolean flag=false;
-
for(Map.Entry<String,Socket> entry:UserTable.returnUserTable().entrySet()){
-
if(entry.getKey().equals(user)){
-
flag=true;
-
return flag;
-
}
-
}
-
return flag;
-
}
-
public static Map<String , Socket> returnUserTable(){
-
return userTable;
-
}
-
public static Socket getSocket(String user){
-
return userTable.get(user);
-
}
-
- }
MsgAnalyseUtil.java
-
package cn.com.xiebiao.smallQQServer;
-
-
import java.util.regex.Pattern;
-
/**
-
*
- * Title : MsgAnalyseUtil.java
-
* Author : Vibe Xie @
-
* Time : Mar 21, 2015 3:35:16 PM
-
* Copyright: Copyright (c) 2015
-
* Description:分析message的工具类
-
*/
-
public class MsgAnalyseUtil {
-
private String msg;
-
private String sender;
-
private String receiver;
-
private String message;
-
String[] tmp=new String[3];
-
-
public MsgAnalyseUtil(String msg){
-
this.msg=msg;
-
tmp=Pattern.compile(">").split(msg,3);
-
sender=tmp[0];
-
receiver=tmp[1];
-
message=tmp[2];
-
}
-
-
public String getSender() {
-
return sender;
-
}
-
-
public String getReceiver() {
-
return receiver;
-
}
-
-
public String getMessage() {
-
return message;
-
}
- }
客户端代码:
QQClient.java
-
package cn.com.xiebiao.smallQQClient;
-
-
import java.io.BufferedReader;
-
import java.io.BufferedWriter;
-
import java.io.IOException;
-
import java.io.InputStreamReader;
-
import java.io.OutputStreamWriter;
-
import java.io.PipedReader;
-
import java.net.Socket;
-
/**
-
*
- * Title : QQClient.java
-
* Author : Vibe Xie @
-
* Time : Mar 22, 2015 3:27:29 PM
-
* Copyright: Copyright (c) 2015
-
* Description:
-
*/
-
public class QQClient {
-
//服务器域名
-
private static String DOMAIN="localhost";
-
//服务器端口
-
private static int SERVER_PORT=8999;
-
//socket
-
private static Socket socket;
-
//发送者
-
private static String sender;
-
//接收者
-
private static String recerver;
-
//消息
-
private static String msg;
-
//用户是否想退出
-
private static String isLoginOut="n";
-
//接收后台返回好友是否存在指令的管道
-
private static PipedReader pipedReader;
-
//好友是否在线
-
private static boolean isFriendOnline=false;
-
public static void main(String[] args) {
-
// TODO Auto-generated method stub
-
try{
-
socket=new Socket(DOMAIN,SERVER_PORT);
-
-
BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
-
BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
-
//reader仅仅为了用户验证,不用作后台接收消息
-
BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
-
boolean flag=true;
-
-
System.out.printf("/********************************************\n"
-
+ "/*************SmallQQ version1.0*************\n"
-
+ "/*************@Author VibeXie *************\n"
-
+ "/*************@Email vibexie@qq.com**********\n"
-
+ "/********************************************\n");
-
/******************登录验证模块,用户名已存在,则放回fail,否则返回success************************/
-
//开启客户端,发送用户名
-
System.out.print("请输入用户名(临时):");
-
sender=msg=in.readLine();
-
writer.write(msg);
-
writer.newLine();
-
writer.flush();
-
while(reader.readLine().equals("fail")){
-
System.out.print("用户已存在,请输入用户名(临时):");
-
sender=msg=in.readLine();
-
writer.write(msg);
-
writer.newLine();
-
writer.flush();
-
}
-
System.out.println("登录成功,您的用户名为:"+sender);
-
-
//启动接收消息线程
-
new Thread(new ClientThread(socket)).start();
-
//连接管道
-
pipedReader=new PipedReader(ClientThread.getPipedWriter());
-
/******************登录验证模块*************************************************************/
-
-
/******************聊天模块************************/
-
-
while(isLoginOut.equals("n")){
-
-
/************************好友在线验证,在线则开始聊天,否则重新输入好友*****************************************/
-
while(isFriendOnline==false){
-
System.out.print("请输入好友:");
-
recerver=in.readLine();
-
//规范判断好友在线指令格式
-
String instruction="/instruction>"+recerver+">tmp";
-
//发送指令
-
writer.write(instruction);
-
writer.newLine();
-
writer.flush();
-
//通过管道接收后台线程的返回值
-
char[] charReader=new char[1];
-
pipedReader.read(charReader,0,1);
-
-
msg=new String(charReader);
-
//分解消息
-
if(msg.equals("n")){
-
isFriendOnline=false;
-
System.out.println("Sorry,好友"+recerver+"现在不在线,请重新输入好友...");
-
}else {
-
isFriendOnline=true;
-
}
-
}
-
-
System.out.println("与"+recerver+"连接成功,开始聊天!");
-
/************************好友在线验证,在线则开始聊天,否则重新输入好友*****************************************/
-
-
while(flag){
-
-
System.out.print("向"+recerver+"发送消息:");
-
msg=in.readLine();
-
-
if(msg.equalsIgnoreCase("bye") || msg==null){
-
//规范消息格式
-
msg=sender+">"+recerver+">"+msg;
-
writer.write(msg);
-
writer.newLine();
-
writer.flush();
-
-
//判断是否退出
-
System.out.print("结束与"+sender+"的聊天?(y/n):");
-
isLoginOut=in.readLine();
-
while(isLoginOut.equals("n")==false && isLoginOut.equals("y")==false){
-
System.out.printf("您的输入错误,请重新输入\n结束与"+sender+"的聊天?(y/n):");
-
isLoginOut=in.readLine();
-
}
-
if(isLoginOut.equals("n")){
-
flag=true;
-
}else {
-
System.out.println("已退出!!!");
-
flag=false;
-
}
-
-
}else {
-
//规范消息格式
-
msg=sender+">"+recerver+">"+msg;
-
-
writer.write(msg);
-
writer.newLine();
-
writer.flush();
-
}
-
}
-
}
-
/******************聊天模块************************/
-
}catch(Exception ex){
-
ex.printStackTrace();
-
}finally{
-
try {
-
socket.close();
-
pipedReader.close();
-
} catch (IOException e) {
-
// TODO Auto-generated catch block
-
e.printStackTrace();
-
}
-
}
-
}
- }
ClientThread.java
-
package cn.com.xiebiao.smallQQClient;
-
-
import java.io.BufferedReader;
-
import java.io.IOException;
-
import java.io.InputStreamReader;
-
import java.io.PipedWriter;
-
import java.net.Socket;
-
-
/**
-
*
- * Title : ClientThread.java
-
* Author : Vibe Xie @
-
* Time : Mar 22, 2015 3:28:08 PM
-
* Copyright: Copyright (c) 2015
-
* Description:
-
*/
-
public class ClientThread implements Runnable{
-
private Socket socket;
-
private static String sender;
-
private static String msg;
-
private static String message;
-
private static boolean running=true;
-
//通往QQClient的管道
-
private static PipedWriter pipedWriter=new PipedWriter();
-
public static PipedWriter getPipedWriter() {
-
return pipedWriter;
-
}
-
-
public ClientThread(Socket socket) {
-
// TODO Auto-generated constructor stub
-
this.socket=socket;
-
}
-
-
@Override
-
public void run() {
-
// TODO Auto-generated method stub
-
try {
-
BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
-
while(running){
-
msg=reader.readLine();
-
MsgAnalyseUtil msgAnalyseUtil=new MsgAnalyseUtil(msg);
-
sender=msgAnalyseUtil.getSender();
-
message=msgAnalyseUtil.getMessage();
-
-
//得到回复用户是否在线的指令,通过管道回写给QQClient
-
if(sender.equals("/instruction")){
-
if(message.equals("bye")){
-
running=false;
-
}else {
-
pipedWriter.write(message);
-
}
-
}else {
-
System.out.printf("\n来自"+sender+"的信息:"+message+"\n");
-
}
-
}
-
} catch (IOException e) {
-
// TODO Auto-generated catch block
-
e.printStackTrace();
-
}
-
-
}
- }