Chinaunix首页 | 论坛 | 博客
  • 博客访问: 408829
  • 博文数量: 121
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1393
  • 用 户 组: 普通用户
  • 注册时间: 2014-03-11 12:17
个人简介

www.vibexie.com vibexie@qq.com

文章分类

全部博文(121)

文章存档

2015年(55)

2014年(66)

我的朋友

分类: Java

2015-03-22 15:33:08

用了一天半的时间,终于还是把客户端聊天的功能实现了,刚开始的时侯,考虑客户端处于内网和外网的因素,肯定不能用UDP通过IP地址来传送消息,
不过也可以通过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

  1. package cn.com.xiebiao.smallQQServer;

  2. import java.io.IOException;
  3. import java.net.ServerSocket;
  4. import java.net.Socket;

  5. /**
  6.  *
  7.  * Title : QQServer.java 
  8.  * Author : Vibe Xie @
  9.  * Time : Mar 22, 2015 3:24:07 PM
  10.  * Copyright: Copyright (c) 2015
  11.  * Description:
  12.  */

  13. public class QQServer {
  14.     private static ServerSocket serverSocket=null;
  15.     private static Socket socket=null;
  16.     //服务器端口
  17.     private static int SERVER_PORT=8999;
  18.     //服务次数
  19.     private static int SERVER_TIMES=1;
  20.     
  21.     public static void main(String[] args) {
  22.         // TODO Auto-generated method stub    
  23.         try {
  24.             serverSocket=new ServerSocket(SERVER_PORT);
  25.             System.out.println("QQ服务器启动...");
  26.             while(true){
  27.                 socket=serverSocket.accept();
  28.                 System.out.println("服务器第"+(SERVER_TIMES++)+"次连接");
  29.                 new Thread(new ServerThread(socket)).start();
  30.             }
  31.             
  32.         } catch (IOException e) {
  33.             // TODO Auto-generated catch block
  34.             e.printStackTrace();
  35.         }finally{
  36.             try {
  37.                 serverSocket.close();
  38.             } catch (IOException e) {
  39.                 // TODO Auto-generated catch block
  40.                 e.printStackTrace();
  41.             }
  42.         }

  43.     }
  44. }

ServerThread.java

  1. package cn.com.xiebiao.smallQQServer;

  2. import java.io.BufferedInputStream;
  3. import java.io.BufferedReader;
  4. import java.io.BufferedWriter;
  5. import java.io.IOException;
  6. import java.io.InputStreamReader;
  7. import java.io.OutputStreamWriter;
  8. import java.net.Socket;
  9. import java.util.Iterator;
  10. import java.util.Map;
  11. import java.util.Set;

  12. /**
  13.  *
  14.  * Title : ClientThread.java 
  15.  * Author : Vibe Xie @
  16.  * Time : Mar 21, 2015 2:34:04 PM
  17.  * Copyright: Copyright (c) 2015
  18.  * Description: ClientThread接收发送端发来的消息,再把消息通过WriteThead线程发送给接收端
  19.  */
  20. public class ServerThread implements Runnable {
  21.     private Socket socket;
  22.     private boolean flag=true;
  23.     //msg的格式是 "senderreceiver"+正文
  24.     private String msg=null;
  25.     private MsgAnalyseUtil msgAnalyse;
  26.     //将msg拆解为sender,receiver,message
  27.     private String sender;
  28.     private String receiver;
  29.     private String message;
  30.     
  31.     private BufferedReader bufferedReader;
  32.     private BufferedWriter writer;
  33.     public ServerThread(Socket socket) {
  34.         // TODO Auto-generated constructor stub
  35.         this.socket=socket;
  36.     }
  37.     
  38.     @Override
  39.     public void run() {
  40.         // TODO Auto-generated method stub
  41.         try {
  42.             
  43.             /******************登录验证模块,用户名已存在,则放回fail,否则返回success************************/
  44.             bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
  45.             writer=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
  46.             //连接成功则从socket读取用户名
  47.             sender=bufferedReader.readLine();
  48.             //用户已存在,返回登录失败信息
  49.             while(UserTable.isUserExist(sender)==true){
  50.                 writer.write("fail");
  51.                 writer.newLine();
  52.                 writer.flush();
  53.                 sender=bufferedReader.readLine();
  54.             }
  55.             
  56.             //将用户名和socket保存到用户表中
  57.             UserTable.userAdd(sender, socket);
  58.             //收到的用户名不存在,返回登录成功
  59.             writer.write("success");
  60.             writer.newLine();
  61.             writer.flush();
  62.             /******************登录验证模块*************************************************************/
  63.             
  64.             while(flag==true){
  65.                 msg=bufferedReader.readLine();
  66.                 //进行拆解
  67.                 System.out.println("服务器接收"+msg);
  68.                 msgAnalyse=new MsgAnalyseUtil(msg);
  69.                 sender=msgAnalyse.getSender();
  70.                 receiver=msgAnalyse.getReceiver();
  71.                 message=msgAnalyse.getMessage();
  72.                 
  73.                 //接收到验证好友是否在线指令
  74.                 if(sender.equals("/instruction")){
  75.                     //在UserTable表中查询好友是否在线,并重新包装消息,返回给请求者
  76.                     if(UserTable.isUserExist(receiver)){
  77.                         message="y";
  78.                     }else {
  79.                         message="n";
  80.                     }
  81.                     //包装消息
  82.                     msg=sender+""+"tmp"+message;
  83.                     writer.write(msg);
  84.                     writer.newLine();
  85.                     writer.flush();
  86.                 }else{
  87.                     //未接收到指令,进行消息转发
  88.                     //服务器输出消息
  89.                     System.out.println("解析为 发送者:"+sender+" 接收者:"+receiver+" 信息:"+message);
  90.                     
  91.                     if(message.equals("bye")){
  92.                         System.out.println("用户退出");
  93.                         
  94.                         //退出是通知客户端后台线程结束
  95.                         msg="/instruction"+""+"tmp"+"bye";
  96.                         writer.write(msg);
  97.                         writer.newLine();
  98.                         writer.flush();
  99.                         
  100.                         //从UserTable中删除该用户
  101.                         UserTable.deleteUser(sender);
  102.                         
  103.                         //结束服务器线程
  104.                         flag=false;
  105.                     }else {
  106.                         //转发消息给接收者
  107.                         writer=new BufferedWriter(new OutputStreamWriter(UserTable.getSocket(receiver).getOutputStream()));
  108.                         writer.write(msg);
  109.                         writer.newLine();
  110.                         writer.flush();
  111.                     }
  112.                 }
  113.             }
  114.         } catch (Exception e) {
  115.             // TODO: handle exception
  116.             e.printStackTrace();
  117.         }
  118.         
  119.     }
  120. }

UserTable.java

  1. package cn.com.xiebiao.smallQQServer;

  2. import java.net.Socket;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. /**
  6.  *
  7.  * Title : UserTable.java 
  8.  * Author : Vibe Xie @
  9.  * Time : Mar 22, 2015 3:25:35 PM
  10.  * Copyright: Copyright (c) 2015
  11.  * Description:
  12.  */
  13. class UserTable{
  14.     private static Map<String, Socket> userTable=new HashMap<String, Socket>();
  15.     
  16.     public static void userAdd(String user,Socket socket){
  17.         userTable.put(user,socket);
  18.     }
  19.     
  20.     public static void deleteUser(String user){
  21.         for(Map.Entry<String,Socket> entry:UserTable.returnUserTable().entrySet()){
  22.             if(entry.getKey().equals(user)){
  23.                 userTable.remove(entry.getKey());
  24.             }
  25.         }
  26.     }
  27.     
  28.     public static boolean isUserExist(String user){
  29.         boolean flag=false;
  30.         for(Map.Entry<String,Socket> entry:UserTable.returnUserTable().entrySet()){
  31.             if(entry.getKey().equals(user)){
  32.                 flag=true;
  33.                 return flag;
  34.             }
  35.         }
  36.         return flag;
  37.     }
  38.     public static Map<String , Socket> returnUserTable(){
  39.         return userTable;
  40.     }
  41.     public static Socket getSocket(String user){
  42.         return userTable.get(user);
  43.     }

  44. }


MsgAnalyseUtil.java

  1. package cn.com.xiebiao.smallQQServer;

  2. import java.util.regex.Pattern;
  3. /**
  4.  *
  5.  * Title : MsgAnalyseUtil.java 
  6.  * Author : Vibe Xie @
  7.  * Time : Mar 21, 2015 3:35:16 PM
  8.  * Copyright: Copyright (c) 2015
  9.  * Description:分析message的工具类
  10.  */
  11. public class MsgAnalyseUtil {
  12.     private String msg;
  13.     private String sender;
  14.     private String receiver;
  15.     private String message;
  16.     String[] tmp=new String[3];
  17.     
  18.     public MsgAnalyseUtil(String msg){
  19.         this.msg=msg;
  20.         tmp=Pattern.compile("").split(msg,3);
  21.         sender=tmp[0];
  22.         receiver=tmp[1];
  23.         message=tmp[2];
  24.     }
  25.     
  26.     public String getSender() {
  27.         return sender;
  28.     }
  29.     
  30.     public String getReceiver() {
  31.         return receiver;
  32.     }
  33.     
  34.     public String getMessage() {
  35.         return message;
  36.     }
  37. }

客户端代码:

QQClient.java

  1. package cn.com.xiebiao.smallQQClient;

  2. import java.io.BufferedReader;
  3. import java.io.BufferedWriter;
  4. import java.io.IOException;
  5. import java.io.InputStreamReader;
  6. import java.io.OutputStreamWriter;
  7. import java.io.PipedReader;
  8. import java.net.Socket;
  9. /**
  10.  *
  11.  * Title : QQClient.java 
  12.  * Author : Vibe Xie @
  13.  * Time : Mar 22, 2015 3:27:29 PM
  14.  * Copyright: Copyright (c) 2015
  15.  * Description:
  16.  */
  17. public class QQClient {
  18.     //服务器域名
  19.     private static String DOMAIN="localhost";
  20.     //服务器端口
  21.     private static int SERVER_PORT=8999;
  22.     //socket
  23.     private static Socket socket;
  24.     //发送者
  25.     private static String sender;
  26.     //接收者
  27.     private static String recerver;
  28.     //消息
  29.     private static String msg;
  30.     //用户是否想退出
  31.     private static String isLoginOut="n";
  32.     //接收后台返回好友是否存在指令的管道
  33.     private static PipedReader pipedReader;
  34.     //好友是否在线
  35.     private static boolean isFriendOnline=false;
  36.     public static void main(String[] args) {
  37.         // TODO Auto-generated method stub
  38.         try{
  39.             socket=new Socket(DOMAIN,SERVER_PORT);
  40.             
  41.             BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
  42.             BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
  43.             //reader仅仅为了用户验证,不用作后台接收消息
  44.             BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
  45.             boolean flag=true;
  46.             
  47.             System.out.printf("/********************************************\n"
  48.                             + "/*************SmallQQ version1.0*************\n"
  49.                             + "/*************@Author VibeXie *************\n"
  50.                             + "/*************@Email vibexie@qq.com**********\n"
  51.                             + "/********************************************\n");
  52.             /******************登录验证模块,用户名已存在,则放回fail,否则返回success************************/
  53.             //开启客户端,发送用户名
  54.             System.out.print("请输入用户名(临时):");
  55.             sender=msg=in.readLine();
  56.             writer.write(msg);
  57.             writer.newLine();
  58.             writer.flush();
  59.             while(reader.readLine().equals("fail")){
  60.                 System.out.print("用户已存在,请输入用户名(临时):");
  61.                 sender=msg=in.readLine();
  62.                 writer.write(msg);
  63.                 writer.newLine();
  64.                 writer.flush();
  65.             }
  66.             System.out.println("登录成功,您的用户名为:"+sender);
  67.             
  68.             //启动接收消息线程
  69.             new Thread(new ClientThread(socket)).start();
  70.             //连接管道
  71.             pipedReader=new PipedReader(ClientThread.getPipedWriter());
  72.             /******************登录验证模块*************************************************************/
  73.             
  74.             /******************聊天模块************************/
  75.             
  76.             while(isLoginOut.equals("n")){
  77.                 
  78.                 /************************好友在线验证,在线则开始聊天,否则重新输入好友*****************************************/
  79.                 while(isFriendOnline==false){
  80.                     System.out.print("请输入好友:");
  81.                     recerver=in.readLine();
  82.                     //规范判断好友在线指令格式
  83.                     String instruction="/instruction"+recerver+"tmp";
  84.                     //发送指令
  85.                     writer.write(instruction);
  86.                     writer.newLine();
  87.                     writer.flush();
  88.                     //通过管道接收后台线程的返回值
  89.                     char[] charReader=new char[1];
  90.                     pipedReader.read(charReader,0,1);
  91.                     
  92.                     msg=new String(charReader);
  93.                     //分解消息
  94.                     if(msg.equals("n")){
  95.                         isFriendOnline=false;
  96.                         System.out.println("Sorry,好友"+recerver+"现在不在线,请重新输入好友...");
  97.                     }else {
  98.                         isFriendOnline=true;
  99.                     }
  100.                 }
  101.                 
  102.                 System.out.println("与"+recerver+"连接成功,开始聊天!");
  103.                 /************************好友在线验证,在线则开始聊天,否则重新输入好友*****************************************/
  104.     
  105.                 while(flag){
  106.                     
  107.                     System.out.print("向"+recerver+"发送消息:");
  108.                     msg=in.readLine();
  109.                     
  110.                     if(msg.equalsIgnoreCase("bye") || msg==null){
  111.                         //规范消息格式
  112.                         msg=sender+""+recerver+""+msg;
  113.                         writer.write(msg);
  114.                         writer.newLine();
  115.                         writer.flush();
  116.                         
  117.                         //判断是否退出
  118.                         System.out.print("结束与"+sender+"的聊天?(y/n):");
  119.                         isLoginOut=in.readLine();
  120.                         while(isLoginOut.equals("n")==false && isLoginOut.equals("y")==false){
  121.                             System.out.printf("您的输入错误,请重新输入\n结束与"+sender+"的聊天?(y/n):");
  122.                             isLoginOut=in.readLine();
  123.                         }
  124.                         if(isLoginOut.equals("n")){
  125.                             flag=true;
  126.                         }else {
  127.                             System.out.println("已退出!!!");
  128.                             flag=false;
  129.                         }
  130.                         
  131.                     }else {
  132.                         //规范消息格式
  133.                         msg=sender+""+recerver+""+msg;
  134.                         
  135.                         writer.write(msg);
  136.                         writer.newLine();
  137.                         writer.flush();
  138.                     }
  139.                 }
  140.             }
  141.             /******************聊天模块************************/
  142.         }catch(Exception ex){
  143.             ex.printStackTrace();
  144.         }finally{
  145.             try {
  146.                 socket.close();
  147.                 pipedReader.close();
  148.             } catch (IOException e) {
  149.                 // TODO Auto-generated catch block
  150.                 e.printStackTrace();
  151.             }
  152.         }
  153.     }
  154. }

ClientThread.java

  1. package cn.com.xiebiao.smallQQClient;

  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.io.PipedWriter;
  6. import java.net.Socket;

  7. /**
  8.  *
  9.  * Title : ClientThread.java 
  10.  * Author : Vibe Xie @
  11.  * Time : Mar 22, 2015 3:28:08 PM
  12.  * Copyright: Copyright (c) 2015
  13.  * Description:
  14.  */
  15. public class ClientThread implements Runnable{
  16.     private Socket socket;
  17.     private static String sender;
  18.     private static String msg;
  19.     private static String message;
  20.     private static boolean running=true;
  21.     //通往QQClient的管道
  22.     private static PipedWriter pipedWriter=new PipedWriter();
  23.     public static PipedWriter getPipedWriter() {
  24.         return pipedWriter;
  25.     }
  26.     
  27.     public ClientThread(Socket socket) {
  28.         // TODO Auto-generated constructor stub
  29.         this.socket=socket;
  30.     }
  31.     
  32.     @Override
  33.     public void run() {
  34.         // TODO Auto-generated method stub
  35.         try {
  36.             BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
  37.             while(running){
  38.                 msg=reader.readLine();
  39.                 MsgAnalyseUtil msgAnalyseUtil=new MsgAnalyseUtil(msg);
  40.                 sender=msgAnalyseUtil.getSender();
  41.                 message=msgAnalyseUtil.getMessage();
  42.                 
  43.                 //得到回复用户是否在线的指令,通过管道回写给QQClient
  44.                 if(sender.equals("/instruction")){
  45.                     if(message.equals("bye")){
  46.                         running=false;
  47.                     }else {
  48.                         pipedWriter.write(message);
  49.                     }
  50.                 }else {
  51.                     System.out.printf("\n来自"+sender+"的信息:"+message+"\n");
  52.                 }
  53.             }
  54.         } catch (IOException e) {
  55.             // TODO Auto-generated catch block
  56.             e.printStackTrace();
  57.         }

  58.     }
  59. }
另外客户端还有一个文件MsgAnalyseUtil.java是服务器端是一样的。



阅读(1967) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~