只有群聊的聊天室里,我们只需要客户端向服务端发送信息,服务端接受信息,并向所有客户端反馈接受到的信息,逻辑非常的简介明了,看似添加私聊功能不会有什么难度,可是私聊功能需要增加的板块却没有那么简单!!! 为什么?
在没有私聊功能的聊天室中,不存在用户名,用户们只需要将通知通知到所有人,所以我们发送的消息不需要被辨别是私信还是对所有人可见的信息,但是在可私聊的聊天室中,我们首先要做的就是在一个客户端连接到服务端的时候提示用户注册,当用户需要发送私信时,需要服务端将信息进行辨别与处理! 若是消息被辨别为群聊,则向所有Socket客户端发送信息,若是私聊信息,我们需要获取的就是消息将要送达的客户端,同时只向该客户端发送消息,那么知道这么基本的思路,让我们来看看流程图理解理解!
从上图中,我们可以清楚的看出,与上一篇中的聊天室大同小异,客户端与服务端的连接基本没有改变,但是从创建客户端时,多了一步操作,就是创建用户名,其实在上图中创建用户名也是需要向服务端反馈的,因为我们需要判断用户名是否重复,但是为了方便各位姥爷理解,我们将用户名重复这个流程给简化.
基本理解流程后我们就可以开始编写代码了,首当其冲的是需要编写一个Map用于储存用户名与输出流的对应关系(或者与Socket客户端),以便我们能通过用户名准确的向对应的客户端传输信息! 切记Map需要使用Collections工具获得线程安全的Map;
import javax.management.RuntimeErrorException;
import java.util.*;
public class CrazyitMap {
public Map map = Collections.synchronizedMap(new HashMap());//创建一个线程安全的map
public synchronized void removeValue(Object value) {//移除对应输出流的元素
for(Object key : map.keySet()) {
if(map.get(key) == value) {
map.remove(key);
break;
}
}
}
public synchronized Set valueSet() { // 遍历主map获得一个输出流集合
Set result = new HashSet();
map.forEach((key,value) -> result.add(value));
return result;
}
public synchronized K getKeyByValue(V value) {//获取输出流对应的NAME
for(K key : map.keySet()) {//得到对应输出流的NAME;
if(map.get(key) == value || map.get(key).equals(value)) {
return key;
}
}
return null;
}
public synchronized V put(K key,V value) {//放置元素
for(V val : valueSet()) {//与集合里的元素逐一比较
if(value.equals(val) && val.hashCode() == value.hashCode()) {
throw new RuntimeException("MapSet has Repetitive value");
}
}
return map.put(key,value);
}
}
Map中的方法包括通过1.输出流获取用户名 2.通过用户名获取输出流. 3.能放入用户名与输出流的元素. 3.能通过遍历Map得到一个输出流的集合(用于向所有用户发送信息). 4.能删除任意输出流对应的元素(将关闭的Socket客户端移除,保证程序正常运行!)
在得到了必要的储存集合后,我们还需要做一个很重要的操作,是的,电脑并不知道我们是想要私发还是想要群发,我们需要一点特定的标识来让服务端知道我们发送的消息是私发还是群发,为此,我们编写了一个没有方法只有成员变量的接口,如下图:
public interface CrazyitProtocal {
int PROTOCAL_LEN = 2;//修饰符长度
String MSG_ROUND = "$&";//全体消息修饰符
String USER_ROUND = "%*";//用户名修饰符
String LOGIN_SUCCESS = "1";//登陆成功反馈
String NAME_REP = "-1";//用户名重复反馈,登陆失败
String PRIVATE_ROUND = "**";//私人消息修饰符
String SPLIT_SIGN = "&";//用户名消息分隔符
}
到底怎么样使用这些标识符,在下面的代码中我会详细的注释出来.
import javax.swing.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client implements CrazyitProtocal{
private static final int SERVER_PORT = 30000;
private Socket socket;
private PrintStream ps;
private BufferedReader brServer;
private BufferedReader keyIn;
public void init() {
try {
keyIn = new BufferedReader(new InputStreamReader(System.in));//创建一个由键盘输入的包装流
socket = new Socket("127.0.0.1",SERVER_PORT);//获取一个Socket连接服务端
ps = new PrintStream(socket.getOutputStream());//通过Socket创建一个输出流
brServer = new BufferedReader(new InputStreamReader(socket.getInputStream()));//通过Socket获取一个输入流读取服务端数据
String tip = "";//创建一个用于提示的String
while(true) {
String userName = JOptionPane.showInputDialog(tip + "Input username");//通过新提示框获取Name;
ps.println(USER_ROUND + userName + USER_ROUND);//输出数据到服务端
String result = brServer.readLine();//从服务端获取反馈
if(result.equals(NAME_REP)) {
tip = "username repetitive! Please input again!";//重置tip提示 并continue在窗口输出要求重新输入用户名Name
continue;
}
if(result.equals(LOGIN_SUCCESS)) {
break;
}
}
}
catch (UnknownHostException e) {
System.out.println("connot find the host!");
closeRs();
System.exit(1);
}
catch (IOException a) {
System.out.println("internet error!");
closeRs();
System.exit(1);
}
new ClientThread(brServer).start();//启用新线程;将br输入流添加给新线程;在线程中进行输入操作
}
private void readAndSend() {
try {
String line = null;
while((line = keyIn.readLine())!= null) {
if(line.indexOf(":") > 0 && line.startsWith ("//")) {//结果为真,发送私聊,对消息处理
line = line.substring(2);//去除反斜线
ps.println(PRIVATE_ROUND +line.split(":")[0]+SPLIT_SIGN+line.split(":")[1]+PRIVATE_ROUND);
}//向服务端发送被修饰的消息
else {//结果为假,群聊
ps.println(MSG_ROUND + line + MSG_ROUND);
}
}
}
catch (IOException e) {//捕获到IO错误,网络连接出错
System.out.println("internet error!");
closeRs();//排错,关闭输入输出流.
System.exit(1);
}
}
private void closeRs() {
try {
if(keyIn != null) {
ps.close();
}
if(brServer != null) {
ps.close();
}
if(ps != null) {
ps.close();
}
if(socket != null) {
keyIn.close();
}
}
catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Client client = new Client();
client.init();
client.readAndSend();
}
}
首先实现的是Client客户端,如我们流程图中所说,我们创建Client实例时,先调用了创建用户名的方法,随后调用获取键盘输入的方法,但是仔细看代码的同学们可能会发现,我们在首次创建用户名的时候,向服务端输入的并不是直接的用户名,而是在用户名的前后加入了标识符,这就是我们实现私聊的精华所在,将带有标识符的数据交给服务端,服务端将判断这条消息是1.创建用户名的消息.2.发送的私聊消息.3.发送的群聊消息. 关于如何识别标识符我们使用的是String对象的split函数,与startWiths,endsWiths函数,在后面的Server客户端类中会有详细标明. 在创建用户名之后,服务端会想客户端反馈是否创建成功,如果成功,进入下一步的获取用户输入的消息,我们就可以启用一个ClientThread线程来与ServerThread线程互动,ClientThread类的实现代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.net.Socket;
public class ClientThread extends Thread{//客户端线程,启动后用于接受来自服务端的输入,对消息进行输出.
BufferedReader br ;
public ClientThread(BufferedReader br) {
this.br = br;
}
public void run() {
try {
String line = null;
while((line = br.readLine())!= null) {//输出来自服务端经过处理后的输入
System.out.println(line);
}
}
catch (IOException e) {
e.printStackTrace();
}
finally {
try {
if(br!=null) {
br.close();
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
这次的ClientThread与上次的比较并没有太大的区别,只是加入了对异常的处理,增强了程序的健壮性.
接下来最最重要的代码要来了!!!! 关于ServerClient类的实现代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
public class ServerThread extends Thread implements CrazyitProtocal{
private Socket s = null;
private BufferedReader br = null;
private PrintStream ps = null;
public ServerThread(Socket s) throws Exception{
this.s = s;
}
public void run(){
String line = null;
try {
br = new BufferedReader(new InputStreamReader(s.getInputStream()));//以Socket创建对饮的输入流,获取客户端输入的信息
ps = new PrintStream(s.getOutputStream());//创建输出流向客户端输出
while((line = br.readLine()) != null) {
if(line.startsWith(USER_ROUND) && line.endsWith(USER_ROUND)) {//判断客户端消息是否为创建用户类型
String userName = getRealMsg(line);//获取用户名
if(Server.clients.map.containsKey(userName)) {//对用户名进行判断
System.out.println("Repetitive!");//提示创建失败
ps.println(NAME_REP);//向客户端反馈
}
else {
System.out.println("SUCCESS!");//提示创建成功
ps.println(LOGIN_SUCCESS);//向客户端反馈
Server.clients.put(userName,ps);//向map创建对应用户名的输出流
}
}
else if (line.startsWith(PRIVATE_ROUND) && line.endsWith(PRIVATE_ROUND)) {//向私聊客户端发送私聊信息
String msgIn = getRealMsg(line);
String user = msgIn.split(SPLIT_SIGN)[0];//将用户名和消息分离为两部分 [0]为用户名 [1]为消息
String msgTo = msgIn.split(SPLIT_SIGN)[1];
Server.clients.map.get(user).println(Server.clients.getKeyByValue(ps) + " speak privately :" +msgTo);//发送消息
}
else {//向全体客户端发送消息
String msg = getRealMsg(line);
for(PrintStream clientPS : Server.clients.valueSet()) {//通过map遍历输出流,向全体客户端发送消息
clientPS.println(Server.clients.getKeyByValue(ps) + " say: "+ msg);
}
}
}
}
catch(IOException e) {//当捕获IOException时,将此输出流移除图,并关闭对应输出流输入流与Socket.
Server.clients.removeValue(ps);
System.out.println(Server.clients.map.size());
try {
if(br != null) {
br.close();
}
if(ps != null) {
ps.close();
}
if(s != null) {
s.close();
}
}
catch(IOException a){
a.printStackTrace();
}
}
}
public String getRealMsg(String line) {//对消息处理,取出左右两边的修饰符号.
return line.substring(PROTOCAL_LEN,line.length()-PROTOCAL_LEN);
}
}
实现逻辑在创建客户端时已经讲过,就不再赘述,主要需要理解的还是对String对象的处理,处理后就可以向客户端发送消息了!
至此我们只差启用服务端就大功告成了.相信看到这里大家也累了,我就直接放出代码了,同样与之前的服务端基本一样,只是加入了异常处理.
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
private final int SERVER_PORT = 30000;
public static CrazyitMap clients = new CrazyitMap<>();
public void init(){
try {
ServerSocket ss = new ServerSocket(SERVER_PORT);
while(true) {
Socket s = ss.accept();//获取连接服务器的对饮的Socket
new ServerThread(s).start();//创建对应的Thread线程响应Socket.
}
}
catch(Exception e) {//接受当端口发生错误时的异常,并提示用户
System.out.println("Faild!/nPlease check the port!");
}
}
public static void main(String[] args) {
Server server = new Server();//启用服务端
server.init();//运行服务端主程序
}
}
至此,我们的聊天室再次大功告成,╮(╯▽╰)╭,我为啥要说再次.让我们康康实现效果 .
写啦这么久,我的手都要累死啦,各位看客老爷们记得留下你们宝贵的点赞鸭!!!!!