java网络编程:案例四:多个客户端群聊(代码片段)
需求:模拟聊天室群聊
客户端要先登录,登录成功之后才能发送和接收消息
分析:
- 服务器端,需要为每个客户端开启一个线程通信,这样才能实现多个客户端“同时”与服务器通信
- 客户端,需要把收消息功能与发消息功能分开两个线程,这样才能“同时收发”,即可以发消息,也可以接收其他客户端的聊天消息
- 服务器端要分别处理客户端的“登录”、“退出”、“聊天”的消息,所以这里设计了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,可以让服务器使用一个或有限几个线程来同时处理连接到服务器上的所有客户端。
相关内容
c/s模型之tcp群聊
说明:
利用TCP协议和多线程实现群聊功能。一个服务器,多个客户端(同一个程序多次启动)。客户端向服务端发送数据,由服务端进行转发到其他
客户端。
/服务端 // WSASever.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <WinSock2.h> #include <Windows.h> #include <vector> #pragma comment (lib,"wSock32.lib") SOCKET sockLink; SOCKET g_psockSockLink[1024] = {0}; //存放客户端的sock int g_nSocketNum=0; //记录客户端sock的数目 //多线程进行接受和转发 DWORD WINAPI SeverThread(LPVOID lParam) { int nErr = 0; char pSeverBuff[MAXBYTE] = { 0 }; //接受客户端的数据 char pSendBuff[MAXBYTE] = { 0 }; //显示在窗口,包括来自哪个IP地址,端口号,数据 SOCKET sockLink = (SOCKET)lParam; //当前的客户端sock SOCKADDR_IN sockAddr; int len = sizeof(SOCKADDR_IN); while (TRUE) { //接受客户端 nErr = recv(sockLink,pSeverBuff, MAXBYTE, 0); if (nErr == SOCKET_ERROR) { break; return -1; } //根据sock获取sock地址 getpeername(sockLink, (sockaddr*)&sockAddr, &len); //将Ip、端口号、数据存入pSendBuff sprintf_s(pSendBuff,"%s(%d):%s ", inet_ntoa(sockAddr.sin_addr), ntohs(sockAddr.sin_port), pSeverBuff); //显示在窗口 printf("%s ", pSendBuff); //转发 for (int i = 0;i<g_nSocketNum;++i) { //不为当前发送方的sock if (g_psockSockLink[i] != sockLink) { send(g_psockSockLink[i], pSendBuff, MAXBYTE, 0); } } } //当客户端关闭时,服务端也随之关闭 //if (nErr == INVALID_SOCKET) //return-1; return 0; } int _tmain(int argc, _TCHAR* argv[]) { //版本检测 WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD(2, 2); err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) { printf("WSAStartup failed with error: %d ", err); return 1; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { printf("Could not find a usable version of Winsock.dll "); WSACleanup(); return 1; } else printf("The Winsock 2.2 dll was found okay "); //程序开始 //创建socket->bind-》listen->accept->recv->send->closesocket SOCKET severSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (severSocket == INVALID_SOCKET) { printf("new socket error!"); } //设置端口号和IP地址、协议。 SOCKADDR_IN sockAddr; sockAddr.sin_port = htons(10086); sockAddr.sin_family = AF_INET; sockAddr.sin_addr.s_addr = htonl (INADDR_ANY); //IP地址表示方法 /*方法1:m_addr.sin_addr.S_un.S_un_b.s_b1 = 192; m_addr.sin_addr.S_un.S_un_b.s_b2 = 168; m_addr.sin_addr.S_un.S_un_b.s_b3 = 0; m_addr.sin_addr.S_un.S_un_b.s_b4 = 1; 方法2: m_addr.sin_addr.S_un.S_un_w.s_w1 = (168 << 8) | 192; m_addr.sin_addr.S_un.S_un_w.s_w2 = (1 << 8) | 0; 方法3: m_addr.sin_addr.S_un.S_addr = (1 << 24) | (0 << 16) | (168 << 8) | 192 方法4; service.sin_addr.s_addr = inet_addr("127.0.0.1"); */ /*sockAddr.sin_addr.S_un.S_un_b.s_b1 = 127; sockAddr.sin_addr.S_un.S_un_b.s_b2 = 0; sockAddr.sin_addr.S_un.S_un_b.s_b3 = 0; sockAddr.sin_addr.S_un.S_un_b.s_b4 = 1;*/ //绑定 if (bind(severSocket, (sockaddr*)&sockAddr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR) { printf("bind error! %d ", WSAGetLastError()); } //监听5个 if (listen(severSocket, 5) == SOCKET_ERROR) { printf("listen error!%d ", WSAGetLastError()); } //创建一个一客户端连接的socket while (true) { //接受来自客户端的sock,并存入客户端的数组中 SOCKET sockLink = accept(severSocket, NULL, NULL); if (sockLink != INVALID_SOCKET) { printf("communication sucess! "); } g_psockSockLink[g_nSocketNum++] = sockLink; //每启动一个客户端,启动一条线程 HANDLE hThread = CreateThread(NULL, 0, SeverThread, (LPVOID)sockLink, 0, NULL); //CloseHandle(hThread); if (hThread == NULL) continue; } closesocket(severSocket); closesocket(sockLink); WSACleanup(); return 0; }
//客户端 // WASClient.cpp : 定义控制台应用程序的入口点。 // //#include <WinSock2.h>一定要在#include <Windows.h>前面 #include "stdafx.h" #include <WinSock2.h> #include <Windows.h> #pragma comment (lib,"wSock32.lib") //用于来自接受服务器的数据,避免的send中造成阻塞。 DWORD WINAPI RectThread(LPVOID lParam) { SOCKET sockLink = (SOCKET)lParam; char pReturnValue[MAXBYTE] = { 0 }; while (true) { recv(sockLink, pReturnValue, MAXBYTE, 0); printf("%s ", pReturnValue); } return 0; } int _tmain(int argc, _TCHAR* argv[]) { WORD wVersionRequested; WSADATA wsaData; int err; //版本检测 wVersionRequested = MAKEWORD(2, 2); err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) { printf("WSAStartup failed with error: %d ", err); return 1; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { printf("Could not find a usable version of Winsock.dll "); WSACleanup(); return 1; } else printf("The Winsock 2.2 dll was found okay "); //程序开始 //创建socket-》连接connect-》发送send-》接受recv-》释放closesocke SOCKET clientSocket=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (clientSocket == INVALID_SOCKET) { printf("new socket error!"); } SOCKADDR_IN sockAddr; //一定要把主机字节序换成网络字节序 并是short类型 htons() sockAddr.sin_port = htons(10086); sockAddr.sin_family = AF_INET; sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //IP地址表示方法 /*方法1: m_addr.sin_addr.S_un.S_un_b.s_b1 = 192; m_addr.sin_addr.S_un.S_un_b.s_b2 = 168; m_addr.sin_addr.S_un.S_un_b.s_b3 = 0; m_addr.sin_addr.S_un.S_un_b.s_b4 = 1; 方法2: m_addr.sin_addr.S_un.S_un_w.s_w1 = (168 << 8) | 192; m_addr.sin_addr.S_un.S_un_w.s_w2 = (1 << 8) | 0; 方法3: m_addr.sin_addr.S_un.S_addr = (1 << 24) | (0 << 16) | (168 << 8) | 192*/ /*sockAddr.sin_addr.S_un.S_un_b.s_b1 = 127; sockAddr.sin_addr.S_un.S_un_b.s_b2 = 0; sockAddr.sin_addr.S_un.S_un_b.s_b3 = 0; sockAddr.sin_addr.S_un.S_un_b.s_b4 = 1; */ //连接 if (connect(clientSocket, (sockaddr*)&sockAddr, sizeof(SOCKADDR_IN)) != SOCKET_ERROR) { printf("communication sucess! "); } char pClientBuf[MAXBYTE] = { 0 }; //存放输入数据 //启动线程 HANDLE hThread = CreateThread(NULL, 0, RectThread, (LPVOID)clientSocket, 0, NULL); if (hThread == NULL) { printf("CreateThread Error num:%d", GetLastError()); CloseHandle(hThread); } CloseHandle(hThread); //请求连接,发送数据 while (TRUE) { gets_s(pClientBuf); int nSendErr=send(clientSocket, pClientBuf, MAXBYTE, 0); if (nSendErr== SOCKET_ERROR) { break; } } WSACleanup(); closesocket(clientSocket); return 0; }
注意点:
1.#include <WinSock2.h>一定要在#include <Windows.h>前面
如:
#include <WinSock2.h>
#include <Windows.h>
2.设定端口号时,一定要把主机字节序换成网络字节序 并是short类型 htons()
sockAddr.sin_port = htons(10086);
3.网络连接的流程:
服务端:创建socket->绑定bind->监听listen->接受客户端的套接字accept->接收recv->发送send->释放closesocket
客户端://创建socket-》连接connect-》发送send-》接受recv-》释放closesocke
4.getpeername(sockLink, (sockaddr*)&sockAddr, &len);
该函数可以根据当前的sock获取对象的sock地址,从而获取对应的IP地址、端口号,协议。
5.IP地址表示方法
方法1: m_addr.sin_addr.S_un.S_un_b.s_b1 = 192;
m_addr.sin_addr.S_un.S_un_b.s_b2 = 168;
m_addr.sin_addr.S_un.S_un_b.s_b3 = 0;
m_addr.sin_addr.S_un.S_un_b.s_b4 = 1;
方法2; service.sin_addr.s_addr = inet_addr("192.168.0.1");
方法3: m_addr.sin_addr.S_un.S_un_w.s_w1 = (168 << 8) | 192; m_addr.sin_addr.S_un.S_un_w.s_w2 = (1 << 8) | 0;
方法4: m_addr.sin_addr.S_un.S_addr = (1 << 24) | (0 << 16) | (168 << 8) | 192
6.SOCKET sockLink = accept(severSocket, NULL, NULL);
accept返回的是一个新的sock,该sock可以与客户端进行连接。就好比服务端与客户端建立一条管道,两者间随时可以进行通信。sockLink与clientSocket
是一对组合。因此不同的客户端启动,将会有不同的sock接入服务端。
7.
问题:客户端为什么专门启动一条线程来接受消息?
解析:首先,该程序是群聊功能,无法确定别人的客户端什么时候回发送消息过来。
其次,如何将send和recv写在同一个while中,当send发送消息后,如果别人客户端没有消息进来,此时就在recv阻塞,直到其他客户端发来消息才会解除,
该客户端才可以继续发送消息,无法实现一个客户端发送多次消息。