需求:模拟聊天室群聊
客户端要先登录,登录成功之后才能发送和接收消息
分析:
- 服务器端,需要为每个客户端开启一个线程通信,这样才能实现多个客户端“同时”与服务器通信
- 客户端,需要把收消息功能与发消息功能分开两个线程,这样才能“同时收发”,即可以发消息,也可以接收其他客户端的聊天消息
- 服务器端要分别处理客户端的“登录”、“退出”、“聊天”的消息,所以这里设计了Code常量类,用状态值区分“登录”、“退出”、“聊天”
- 这里设计Message类,包含code属性,区别是“登录”、“退出”、“聊天”,username属性表示用户名,表明消息是谁发的,content属性,存储消息内容,如果是登录,就用来存储密码。
- 这里消息是Message对象,因此在客户端与服务器之间传输的是对象,所以选择ObjectOutputStream和ObjectInputStream。
- 这里的Message类与Code类是服务器端和客户端“共享”的,要保持一致。特别注意包名和序列化版本ID。
Message 类与 Code 类的示例代码:
1 //Code
2 public class Code {
3 public static final int LOGIN = 1;
4 public static final int CHAT = 2;
5 public static final int LOGOUT = 3;
6
7 public static final int SUCCESS = 1;
8 public static final int FAIL = 2;
9 }
10
11
12 // Message
13 public class Message implements Serializable{
14 private static final long serialVersionUID = 1L;
15 private int code;
16 private String username;
17 private String content;
18 public Message(int code, String username, String content) {
19 super();
20 this.code = code;
21 this.username = username;
22 this.content = content;
23 }
24
25 public Message() {
26 super();
27 }
28
29 public int getCode() {
30 return code;
31 }
32 public void setCode(int code) {
33 this.code = code;
34 }
35 public String getUsername() {
36 return username;
37 }
38 public void setUsername(String username) {
39 this.username = username;
40 }
41 public String getContent() {
42 return content;
43 }
44 public void setContent(String content) {
45 this.content = content;
46 }
47 }
服务器端用户管理类代码:
1 import java.util.HashMap;
2
3 public class UserManager {
4 public static HashMap<String,String> allUsers = new HashMap<String,String>();
5 static{
6 allUsers.put("gangge", "123");
7 allUsers.put("xiaobai", "456");
8 allUsers.put("gujie", "789");
9 }
10
11 public static boolean login(String username, String password){
12 if(allUsers.get(username)!=null && allUsers.get(username).equals(password)){
13 return true;
14 }else{
15 return false;
16 }
17 }
18 }
服务器端实例代码:
1 import java.net.ServerSocket;
2 import java.net.Socket;
3
4 public class Server {
5 public static void main(String[] args)throws Exception {
6 @SuppressWarnings("resource")
7 ServerSocket server = new ServerSocket(9999);
8
9 while(true){
10 Socket socket = server.accept();
11
12 ClientHandlerThread ct = new ClientHandlerThread(socket);
13 ct.start();
14 }
15 }
16 }
服务器端处理消息的线程类代码:
1 import java.io.IOException;
2 import java.io.ObjectInputStream;
3 import java.io.ObjectOutputStream;
4 import java.net.Socket;
5 import java.util.ArrayList;
6 import java.util.Collections;
7 import java.util.HashSet;
8 import java.util.Set;
9
10 import com.tcp.chat.bean.Code;
11 import com.tcp.chat.bean.Message;
12
13 public class ClientHandlerThread extends Thread{
14 public static Set<ObjectOutputStream> online = Collections.synchronizedSet(new HashSet<ObjectOutputStream>());
15
16 private Socket socket;
17 private String username;
18 private ObjectInputStream ois;
19 private ObjectOutputStream oos;
20
21 public ClientHandlerThread(Socket socket) {
22 super();
23 this.socket = socket;
24 }
25
26 public void run(){
27 Message message = null;
28 try{
29 ois = new ObjectInputStream(socket.getInputStream());
30 oos = new ObjectOutputStream(socket.getOutputStream());
31
32 //接收数据
33 while (true) {
34 message = (Message) ois.readObject();
35
36 if(message.getCode() == Code.LOGIN){
37 //如果是登录,则验证用户名密码
38 username = message.getUsername();
39 String password = message.getContent();
40 if(UserManager.login(username, password)){
41 message.setCode(Code.SUCCESS);
42 oos.writeObject(message);
43
44 //并将该用户添加到在线人员名单中
45 online.add(oos);
46
47 message.setCode(Code.CHAT);
48 message.setContent("上线了");
49 //通知其他人,xx上线了
50 sendToOther(message);
51 }else{
52 message.setCode(Code.FAIL);
53 oos.writeObject(message);
54 }
55 }else if(message.getCode() == Code.CHAT){
56 //如果是聊天信息,把消息转发给其他在线客户端
57 sendToOther(message);
58 }else if(message.getCode() == Code.LOGOUT){
59 //通知其他人,xx下线了
60 message.setContent("下线了");
61 sendToOther(message);
62 break;
63 }
64 }
65 }catch(Exception e){
66 //通知其他人,xx掉线了
67 if(message!=null && username!=null){
68 message.setCode(Code.LOGOUT);
69 message.setContent("掉线了");
70 sendToOther(message);
71 }
72 }finally{
73 //从在线人员中移除并断开当前客户端
74 try {
75 online.remove(oos);
76 socket.close();
77 } catch (IOException e) {
78 e.printStackTrace();
79 }
80 }
81 }
82
83 private void sendToOther(Message message) {
84 ArrayList<ObjectOutputStream> offline = new ArrayList<ObjectOutputStream>();
85 for (ObjectOutputStream on : online) {
86 if(!on.equals(oos)){
87 try {
88 on.writeObject(message);
89 } catch (IOException e) {
90 offline.add(on);
91 }
92 }
93 }
94
95 for (ObjectOutputStream off : offline) {
96 online.remove(off);
97 }
98 }
99 }
客户端示例代码:
1 import java.io.ObjectInputStream;
2 import java.io.ObjectOutputStream;
3 import java.net.Socket;
4 import java.util.Scanner;
5
6 import com.tcp.chat.bean.Code;
7 import com.tcp.chat.bean.Message;
8
9 public class Client {
10 public static void main(String[] args) throws Exception{
11 Socket socket = new Socket("192.168.1.107", 9999);
12
13 ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
14 ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
15
16 //先登录
17 Scanner scanner = new Scanner(System.in);
18 String username;
19 while(true){
20 //输入登录信息
21 System.out.println("用户名:");
22 username = scanner.nextLine();
23 System.out.println("密码:");
24 String password = scanner.nextLine();
25
26 Message msg = new Message(Code.LOGIN, username, password);
27 //发送登录数据
28 oos.writeObject(msg);
29 // 接收登录结果
30 msg = (Message) ois.readObject();
31 if(msg.getCode() == Code.SUCCESS){
32 System.out.println("登录成功!");
33 break;
34 }else if(msg.getCode() == Code.FAIL){
35 System.out.println("用户名或密码错误,登录失败,重新输入");
36 }
37 }
38
39 //启动收消息和发消息线程
40 SendThread s = new SendThread(oos,username);
41 ReceiveThread r = new ReceiveThread(ois);
42 s.start();
43 r.start();
44
45 s.join();//不发了,就结束
46 r.setFlag(false);
47 r.join();
48
49 scanner.close();
50 socket.close();
51 }
52 }
客户端发消息线程类代码:
1 import java.io.IOException;
2 import java.io.ObjectOutputStream;
3 import java.util.Scanner;
4
5 import com.tcp.chat.bean.Code;
6 import com.tcp.chat.bean.Message;
7
8 public class SendThread extends Thread{
9 private ObjectOutputStream oos;
10 private String username;
11
12 public SendThread(ObjectOutputStream oos,String username) {
13 super();
14 this.oos = oos;
15 this.username = username;
16 }
17
18 public void run(){
19 try {
20 Scanner scanner = new Scanner(System.in);
21 while(true){
22 System.out.println("请输入消息内容:");
23 String content = scanner.nextLine();
24 Message msg;
25 if("bye".equals(content)){
26 msg = new Message(Code.LOGOUT, username, content);
27 oos.writeObject(msg);
28 scanner.close();
29 break;
30 }else{
31 msg = new Message(Code.CHAT, username, content);
32 oos.writeObject(msg);
33 }
34 }
35 } catch (IOException e) {
36 e.printStackTrace();
37 }
38 }
39 }
客户端接受消息线程类代码:
1 import java.io.ObjectInputStream;
2
3 import com.tcp.chat.bean.Message;
4
5 public class ReceiveThread extends Thread{
6 private ObjectInputStream ois;
7 private volatile boolean flag = true;
8
9 public ReceiveThread(ObjectInputStream ois) {
10 super();
11 this.ois = ois;
12 }
13 public void run(){
14 try {
15 while(flag){
16 Message msg = (Message) ois.readObject();
17 System.out.println(msg.getUsername() + ":" + msg.getContent());
18 }
19 } catch (Exception e) {
20 System.out.println("请重新登录");
21 }
22 }
23 public void setFlag(boolean flag) {
24 this.flag = flag;
25 }
26
27 }
注意:
以上案例的网络通信程序是基于阻塞式API的,所以服务器必须为每个客户端都提供一条独立线程进行处理,当服务器需要同时处理大量客户端时,这种做法会导致性能下降。如果要开发高性能网络服务器,那么需要使用Java提供的NIO API,可以让服务器使用一个或有限几个线程来同时处理连接到服务器上的所有客户端。