Java之Socket简单聊天实现(QQ续二)

来源:互联网 发布:上海知楚 编辑:程序博客网 时间:2024/05/01 09:55

       转载请注明出处,谢谢! 

       今天跟大家分享一下我那QQ小项目中服务器与客户端的核心代码,并谈谈一些我的建议和看法,希望大家多多支持,你们的支持,就是我继续分享的动力,哈哈!

        一、服务器,好了,废话不多说,我们先来看看服务器部分,我这里用到线程池,至于为什么用线程池,不知道的童鞋可以去我的另一篇blog看看:http://blog.csdn.net/weidi1989/article/details/7930820。当一个用户连接上之后,我们马上将该用户的socket丢入已经建好的线程池中去处理,这样可以很快腾出时间来接受下一个用户的连接,而线程池中的这个线程又分支为两个线程,一个是读消息线程,一个是写消息线程,当然,因为我这个聊天是用来转发消息的,所以还以单例模式建了一个Map用来存放每个用户的写消息线程(如果用户多的话,这是相当消耗资源的),以便在转发消息的时候,通过Map的key就可以取出对应用户的写消息线程,从而达到转发消息的目的。具体下面再说

/** * 服务器,接受用户登录、离线、转发消息 *  * @author way *  */public class Server {private ExecutorService executorService;// 线程池private ServerSocket serverSocket = null;private Socket socket = null;private boolean isStarted = true;//是否循环等待public Server() {try {// 创建线程池,池中具有(cpu个数*50)条线程executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 50);serverSocket = new ServerSocket(Constants.SERVER_PORT);} catch (IOException e) {e.printStackTrace();quit();}}public void start() {System.out.println(MyDate.getDateCN() + " 服务器已启动...");try {while (isStarted) {socket = serverSocket.accept();String ip = socket.getInetAddress().toString();System.out.println(MyDate.getDateCN() + " 用户:" + ip + " 已建立连接");// 为支持多用户并发访问,采用线程池管理每一个用户的连接请求if (socket.isConnected())executorService.execute(new SocketTask(socket));// 添加到线程池}if (socket != null)//循环结束后,记得关闭socket,释放资源socket.close();if (serverSocket != null)serverSocket.close();} catch (IOException e) {e.printStackTrace();// isStarted = false;}}private final class SocketTask implements Runnable {private Socket socket = null;private InputThread in;private OutputThread out;private OutputThreadMap map;public SocketTask(Socket socket) {this.socket = socket;map = OutputThreadMap.getInstance();}@Overridepublic void run() {out = new OutputThread(socket, map);//// 先实例化写消息线程,(把对应用户的写线程存入map缓存器中)in = new InputThread(socket, out, map);// 再实例化读消息线程out.setStart(true);in.setStart(true);in.start();out.start();}}/** * 退出 */public void quit() {try {this.isStarted = false;serverSocket.close();} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {new Server().start();}}


二、服务器写消息线程,接下来,我们来看看写消息线程,很简单的一段代码,有注释,我就不多说了:

/** * 写消息线程 *  * @author way *  */public class OutputThread extends Thread {private OutputThreadMap map;private ObjectOutputStream oos;private TranObject object;private boolean isStart = true;// 循环标志位private Socket socket;public OutputThread(Socket socket, OutputThreadMap map) {try {this.socket = socket;this.map = map;oos = new ObjectOutputStream(socket.getOutputStream());// 在构造器里面实例化对象输出流} catch (IOException e) {e.printStackTrace();}}public void setStart(boolean isStart) {//用于外部关闭写线程this.isStart = isStart;}// 调用写消息线程,设置了消息之后,唤醒run方法,可以节约资源public void setMessage(TranObject object) {this.object = object;synchronized (this) {notify();}}@Overridepublic void run() {try {while (isStart) {// 没有消息写出的时候,线程等待synchronized (this) {wait();}if (object != null) {oos.writeObject(object);oos.flush();}}if (oos != null)// 循环结束后,关闭流,释放资源oos.close();if (socket != null)socket.close();} catch (InterruptedException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}}


 三、服务器写消息线程缓存器,接下来让我们看一下那个写消息线程缓存器的庐山真面目:

/** * 存放写线程的缓存器 *  * @author way */public class OutputThreadMap {private HashMap<Integer, OutputThread> map;private static OutputThreadMap instance;// 私有构造器,防止被外面实例化改对像private OutputThreadMap() {map = new HashMap<Integer, OutputThread>();}// 单例模式像外面提供该对象public synchronized static OutputThreadMap getInstance() {if (instance == null) {instance = new OutputThreadMap();}return instance;}// 添加写线程的方法public synchronized void add(Integer id, OutputThread out) {map.put(id, out);}// 移除写线程的方法public synchronized void remove(Integer id) {map.remove(id);}// 取出写线程的方法,群聊的话,可以遍历取出对应写线程public synchronized OutputThread getById(Integer id) {return map.get(id);}// 得到所有写线程方法,用于向所有在线用户发送广播public synchronized List<OutputThread> getAll() {List<OutputThread> list = new ArrayList<OutputThread>();for (Map.Entry<Integer, OutputThread> entry : map.entrySet()) {list.add(entry.getValue());}return list;}}


四、服务器读消息线程,接下来是读消息线程,这里包括两个部分,一部分是读消息,另一部分是处理消息,我以分开的形式贴出代码,虽然我是写在一个类里面的:

/** * 读消息线程和处理方法 *  * @author way *  */public class InputThread extends Thread {private Socket socket;// socket对象private OutputThread out;// 传递进来的写消息线程,因为我们要给用户回复消息啊private OutputThreadMap map;//写消息线程缓存器private ObjectInputStream ois;//对象输入流private boolean isStart = true;//是否循环读消息public InputThread(Socket socket, OutputThread out, OutputThreadMap map) {this.socket = socket;this.out = out;this.map = map;try {ois = new ObjectInputStream(socket.getInputStream());//实例化对象输入流} catch (IOException e) {e.printStackTrace();}}public void setStart(boolean isStart) {//提供接口给外部关闭读消息线程this.isStart = isStart;}@Overridepublic void run() {try {while (isStart) {// 读取消息readMessage();}if (ois != null)ois.close();if (socket != null)socket.close();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}


五、服务器消息处理,下面是处理消息的方法,由于比较麻烦以及各种纠结,我就与读消息线程分开贴,显得稍微简洁一点:

 

/** * 读消息以及处理消息,抛出异常 *  * @throws IOException * @throws ClassNotFoundException */public void readMessage() throws IOException, ClassNotFoundException {Object readObject = ois.readObject();// 从流中读取对象UserDao dao = UserDaoFactory.getInstance();// 通过dao模式管理后台if (readObject != null && readObject instanceof TranObject) {TranObject read_tranObject = (TranObject) readObject;// 转换成传输对象switch (read_tranObject.getType()) {case REGISTER:// 如果用户是注册User registerUser = (User) read_tranObject.getObject();int registerResult = dao.register(registerUser);System.out.println(MyDate.getDateCN() + " 新用户注册:"+ registerResult);// 给用户回复消息TranObject<User> register2TranObject = new TranObject<User>(TranObjectType.REGISTER);User register2user = new User();register2user.setId(registerResult);register2TranObject.setObject(register2user);out.setMessage(register2TranObject);break;case LOGIN:User loginUser = (User) read_tranObject.getObject();ArrayList<User> list = dao.login(loginUser);TranObject<ArrayList<User>> login2Object = new TranObject<ArrayList<User>>(TranObjectType.LOGIN);if (list != null) {// 如果登录成功TranObject<User> onObject = new TranObject<User>(TranObjectType.LOGIN);User login2User = new User();login2User.setId(loginUser.getId());onObject.setObject(login2User);for (OutputThread onOut : map.getAll()) {onOut.setMessage(onObject);// 广播一下用户上线}map.add(loginUser.getId(), out);// 先广播,再把对应用户id的写线程存入map中,以便转发消息时调用login2Object.setObject(list);// 把好友列表加入回复的对象中} else {login2Object.setObject(null);}out.setMessage(login2Object);// 同时把登录信息回复给用户System.out.println(MyDate.getDateCN() + " 用户:"+ loginUser.getId() + " 上线了");break;case LOGOUT:// 如果是退出,更新数据库在线状态,同时群发告诉所有在线用户User logoutUser = (User) read_tranObject.getObject();int offId = logoutUser.getId();System.out.println(MyDate.getDateCN() + " 用户:" + offId + " 下线了");dao.logout(offId);isStart = false;// 结束自己的读循环map.remove(offId);// 从缓存的线程中移除out.setMessage(null);// 先要设置一个空消息去唤醒写线程out.setStart(false);// 再结束写线程循环TranObject<User> offObject = new TranObject<User>(TranObjectType.LOGOUT);User logout2User = new User();logout2User.setId(logoutUser.getId());offObject.setObject(logout2User);for (OutputThread offOut : map.getAll()) {// 广播用户下线消息offOut.setMessage(offObject);}break;case MESSAGE:// 如果是转发消息(可添加群发)// 获取消息中要转发的对象id,然后获取缓存的该对象的写线程int id2 = read_tranObject.getToUser();OutputThread toOut = map.getById(id2);if (toOut != null) {// 如果用户在线toOut.setMessage(read_tranObject);} else {// 如果为空,说明用户已经下线,回复用户TextMessage text = new TextMessage();text.setMessage("亲!对方不在线哦,您的消息将暂时保存在服务器");TranObject<TextMessage> offText = new TranObject<TextMessage>(TranObjectType.MESSAGE);offText.setObject(text);offText.setFromUser(0);out.setMessage(offText);}break;case REFRESH:List<User> refreshList = dao.refresh(read_tranObject.getFromUser());TranObject<List<User>> refreshO = new TranObject<List<User>>(TranObjectType.REFRESH);refreshO.setObject(refreshList);out.setMessage(refreshO);break;default:break;}}}


好了,服务器的核心代码就这么一些了,很简单吧?是的,因为我们还有很多事情没有去做,比如说心跳监测用户是否一直在线,如果不在线,就释放资源等,这些都是商业项目中必须要考虑到的问题,至于这个通过心跳监测用户是否在线,我说说我的一些想法吧:由客户端定时给服务器发送一个心跳包(最好是空包,节约流量),服务器也定时去监测那个心跳包,如果有3次未收到客户端的心跳包,就判断该用户已经掉线,释放资源,至于这次数和时间间隔,就随情况而定了。如果有什么更好的其他建议,欢迎给我留言,谢谢。

 

六、消息传输对象,下面,我们来看看,这个超级消息对象和定义好的消息类型:

/** * 传输的对象,直接通过Socket传输的最大对象 *  * @author way */public class TranObject<T> implements Serializable {/** *  */private static final long serialVersionUID = 1L;private TranObjectType type;// 发送的消息类型private int fromUser;// 来自哪个用户private int toUser;// 发往哪个用户private T object;// 传输的对象,这个对象我们可以自定义任何private List<Integer> group;// 群发给哪些用户get...set...

 

/** * 传输对象类型 *  * @author way *  */public enum TranObjectType {REGISTER, // 注册LOGIN, // 用户登录LOGOUT, // 用户退出登录FRIENDLOGIN, // 好友上线FRIENDLOGOUT, // 好友下线MESSAGE, // 用户发送消息UNCONNECTED, // 无法连接FILE, // 传输文件REFRESH,//刷新好友列表}

 

 

七、客户端,然后是客户端部分了,其实跟服务器差不多,只是没有建立线程池了,因为没有必要,是吧?然后实例化写线程和读线程没有先后顺序,这也勉强算一个区别吧~呵呵

/** * 客户端 *  * @author way *  */public class Client {private Socket client;private ClientThread clientThread;private String ip;private int port;public Client(String ip, int port) {this.ip = ip;this.port = port;}public boolean start() {try {client = new Socket();// client.connect(new InetSocketAddress(Constants.SERVER_IP,// Constants.SERVER_PORT), 3000);client.connect(new InetSocketAddress(ip, port), 3000);if (client.isConnected()) {// System.out.println("Connected..");clientThread = new ClientThread(client);clientThread.start();}} catch (IOException e) {e.printStackTrace();return false;}return true;}// 直接通过client得到读线程public ClientInputThread getClientInputThread() {return clientThread.getIn();}// 直接通过client得到写线程public ClientOutputThread getClientOutputThread() {return clientThread.getOut();}// 直接通过client停止读写消息public void setIsStart(boolean isStart) {clientThread.getIn().setStart(isStart);clientThread.getOut().setStart(isStart);}public class ClientThread extends Thread {private ClientInputThread in;private ClientOutputThread out;public ClientThread(Socket socket) {in = new ClientInputThread(socket);out = new ClientOutputThread(socket);}public void run() {in.setStart(true);out.setStart(true);in.start();out.start();}// 得到读消息线程public ClientInputThread getIn() {return in;}// 得到写消息线程public ClientOutputThread getOut() {return out;}}}


八、客户端写消息线程,先看看客户端写消息线程吧:

/** * 客户端写消息线程 *  * @author way *  */public class ClientOutputThread extends Thread {private Socket socket;private ObjectOutputStream oos;private boolean isStart = true;private TranObject msg;public ClientOutputThread(Socket socket) {this.socket = socket;try {oos = new ObjectOutputStream(socket.getOutputStream());} catch (IOException e) {e.printStackTrace();}}public void setStart(boolean isStart) {this.isStart = isStart;}// 这里处理跟服务器是一样的public void setMsg(TranObject msg) {this.msg = msg;synchronized (this) {notify();}}@Overridepublic void run() {try {while (isStart) {if (msg != null) {oos.writeObject(msg);oos.flush();if (msg.getType() == TranObjectType.LOGOUT) {// 如果是发送下线的消息,就直接跳出循环break;}synchronized (this) {wait();// 发送完消息后,线程进入等待状态}}}oos.close();// 循环结束后,关闭输出流和socketif (socket != null)socket.close();} catch (InterruptedException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}}


九、客户端读消息线程,然后是客户端读消息线程,这里又有一个要注意的地方,我们收到消息的时候,是不是要告诉用户?如何告诉呢?接口监听貌似是一个很好的办法,神马?不知道接口监听?你会用Android的setOnClickListener不?这就是android封装好的点击事件监听,不懂的话,可以好好看看,理解一下,其实也不难:

/** * 客户端读消息线程 *  * @author way *  */public class ClientInputThread extends Thread {private Socket socket;private TranObject msg;private boolean isStart = true;private ObjectInputStream ois;private MessageListener messageListener;// 消息监听接口对象public ClientInputThread(Socket socket) {this.socket = socket;try {ois = new ObjectInputStream(socket.getInputStream());} catch (IOException e) {e.printStackTrace();}}/** * 提供给外部的消息监听方法 *  * @param messageListener *            消息监听接口对象 */public void setMessageListener(MessageListener messageListener) {this.messageListener = messageListener;}public void setStart(boolean isStart) {this.isStart = isStart;}@Overridepublic void run() {try {while (isStart) {msg = (TranObject) ois.readObject();// 每收到一条消息,就调用接口的方法,并传入该消息对象,外部在实现接口的方法时,就可以及时处理传入的消息对象了// 我不知道我有说明白没有?messageListener.Message(msg);}ois.close();if (socket != null)socket.close();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}/** * 消息监听接口 *  * @author way *  */public interface MessageListener {public void Message(TranObject msg);}}

好了,总算copy完了,如果大家有什么不理解,或者有什么好的建议,欢迎给我留言,记住:你们的支持是我继续下去的动力,哈哈
 

原创粉丝点击