实现客户端监控:
目的:在服务器端写线程,让服务器端线程代理客户端的Socket
技术:利用线程池来管理所有的客户端套接字,ExecutorService
package com.zhiliaotang.chat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 网络聊天的服务器端套接字
* 1.对客户端监控
* 2.同时监控到客户端需要用线程代理
* 3.信息转发
* @author Administrator
*
*/
public class TCPServer {
private ServerSocket serverSocket;//创建服务器端的套接字对象
private ExecutorService executorService;//线程池,代理监控到的客户端套接字对象
private Map<String, PrintWriter> storeInfo;//存储客户端的信息,存储客户端的名称+IO流
public TCPServer() {
try {
serverSocket = new ServerSocket(8989);
storeInfo = new HashMap<>();
executorService = Executors.newCachedThreadPool();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 启动服务器端的套接字
*/
public void start() {
try {
while(true) {
System.out.println("等待客户端连接。。。");
Socket socket = serverSocket.accept();//监听客户端的套接字
//获取客户端的ip地址
InetAddress inetAddress = socket.getInetAddress();
System.out.println("客户端:"+inetAddress.getHostAddress()+" 连接成功。。。");
//启动一个线程来代理客户端的套接字对象
executorService.execute(new ListenerClient(socket));//线程代理后,启动线程中run方法
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 存储客户端的聊天信息
* @param key
* @param value
*/
private synchronized void putIn(String key, PrintWriter value) {
storeInfo.put(key, value);
}
/**
* 群聊,发送消息给所有客户端
* @param message
*/
private synchronized void sendToAll(String message) {
/**
* 原来Map接口还可以这样做
*/
for(PrintWriter pw : storeInfo.values()) {
pw.println(message);
}
}
/**
* 私聊,发送消息给指定的客户端
* @param name
* @param message
*/
private synchronized void sendToSomeOne(String name, String message) {
PrintWriter pw = storeInfo.get(name);
if(pw != null) {
pw.println(message);
}
}
/**
* 移出客户端的信息
* @param name
*/
private synchronized void remove(String name) {
storeInfo.remove(name);
System.out.println("当前在线人数:"+storeInfo.size());
}
/**
* 用线程代理客户端的套接字对象
* @author Administrator
*
*/
class ListenerClient implements Runnable{
private Socket socket;//客户端套接字的对象
private String name;//客户端的昵称
public ListenerClient(Socket socket) {
this.socket = socket;
}
/**
* 1.获取输出流,发送消息
* 2.读取客户端的信息,读取客户端的昵称
* 3.检验是群聊还是私聊(客户端如果发送@符号,代表私聊)
* 4.客户端关闭,程序还得把客户端的套接字关闭
*/
@Override
public void run() {
try {
//一定要注意把客户端套接字的流保存storeInfo里面
//必须收集(存储)连接上服务器端的所有套接字对象
//PrintWriter是一个字符流,而我们套接字只能给出字节流
//由getName()方法来获取客户端传递过来的昵称,因为服务器端没有能力给客户端取昵称
PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"),true);
name = getName();
putIn(name, pw);
Thread.sleep(100);
sendToAll("[系统通知]"+name+" 已上线。。。");
//不管是群聊还是私聊都得获取输入流
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
//现在才去读取消息,判断是群聊还是私聊
String message = null;
while((message = br.readLine())!= null) {
//检验是否为私聊(格式、@昵称、内容等),截取字符串
if(message.startsWith("@")) {
int index = message.indexOf(":");
if(index > 0) {
//获取昵称
String theName = message.substring(1, index);
//获取说话内容
String info = message.substring(index+1, message.length());
//组合一下消息内容,让对方看到我的名字和我发送的内容
info = name + ":"+info;
//发送私聊
sendToSomeOne(theName, info);
continue;
}
}
//群聊
//遍历所有输出流,将该客户端发送的信息转发给所有客户端
System.out.println(name+" : "+message);
sendToAll(name+" : "+message);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
remove(name);
System.out.println("[系统消息]"+name+" 已经下线。。。");
if(socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 获取客户端昵称的方法
* 1.客户端连接上服务器
* 2.提示客户端输入一个昵称
* 3.昵称一旦输完,则马上把昵称发送给服务器端
* @return
*/
private String getName() {
/**
* 要获取客户端的昵称
* 1.我们要准备两个流
* 2.PrintWriter
* 3.BufferedReader
* 4.服务器拿到昵称,判断是否有重复昵称,并告知客户端
*/
try {
//服务器发送消息告诉客户端是否你输入的昵称有重复
PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8"),true);
//读取客户端输入的昵称
BufferedReader br =new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
//这是一个连续你来我往的通信
//昵称有问题,请客户端重新写一个
//然后服务器端又重新读取
//又判断是否重复
while(true) {
String name = br.readLine();
if(name.trim().length() == 0 || storeInfo.containsKey(name)) {//昵称不可使用或者重复或者你客户端根本没有输入
pw.println("FALL");
}else {//代表昵称可以使用
pw.println("OK");
return name;
}
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
public static void main(String[] args) {
TCPServer tcpServer = new TCPServer();
tcpServer.start();
}
}
package com.zhiliaotang.chat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class TCPClient {
private static Socket clientSocket;
public TCPClient() throws UnknownHostException, IOException {
System.out.println("设置服务器端的地址:");
Scanner scanner = new Scanner(System.in);
String ip = scanner.nextLine();//输入服务器端的ip地址
clientSocket = new Socket(ip, 8989);
start();
}
/**
* 启动客户端的套接字
* 同时还需要有线程来代理客户端的套接字
* @throws IOException
* @throws UnsupportedEncodingException
*/
private void start() throws UnsupportedEncodingException, IOException {
/**
* 1.连上服务器后需要输入昵称
* 2.客户端设置昵称,必须根据服务器端返回昵称可不可以使用的情况来设置昵称
* 3.发送聊天内容,聊天内容需要线程
*/
Scanner scanner = new Scanner(System.in);
setName(scanner);
//接收服务器端发送过来的信息的线程启动
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ListenerServer());
//建立输出流,给服务端发信息
PrintWriter pw = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"), true);
while(true) {
pw.println(scanner.nextLine());
}
}
/**
* 客户端设置昵称,必须根据服务器端返回昵称可不可以使用的情况来设置昵称
* @param scanner
* @throws IOException
* @throws UnsupportedEncodingException
*/
private void setName(Scanner scanner) throws UnsupportedEncodingException, IOException {
/**
* 1.输入流:读取服务器返回昵称的合法性
* 2.输出流:告诉服务器我输入的昵称
* 3.你来我往的昵称检验
*/
String name;
//创建输出流
PrintWriter pw = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"), true);
//创建输入流
BufferedReader br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
while(true) {
System.out.println("请输入你的昵称:");
name = scanner.nextLine();
if(name.trim().length() == 0) {
System.out.println("昵称不能为空。。。");
}else {
pw.println(name);//把昵称发送给服务器端,服务器端getName()来接收你的昵称
String pass = br.readLine();//读取服务器端的昵称是否可以使用,要么OK,要么FALL
if(pass != null && (!pass.equals("OK"))) {
System.out.println("昵称已经被占用,请重新输入:");
}else {
System.out.println("昵称"+name+" 已设置成功,可以开始聊天了");
break;
}
}
}
}
/**
* 专门监听服务器端返回的消息
* 这个消息针对群聊或私聊信息
* 线程里面只提供输入流来读取信息
* @author Administrator
*
*/
class ListenerServer implements Runnable{
@Override
public void run() {
BufferedReader br;
try {
br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
String msgString;
while((msgString = br.readLine())!=null) {
System.out.println(msgString);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws UnknownHostException, IOException {
new TCPClient();
}
}
请关注“知了堂学习社区”,地址:http://www.zhiliaotang.com/portal.php