Server端
package day05.chat;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 服务端应用程序
* */
public class Server {
/**
* 运行在服务端的Socket
* ServerSocket可以用来接收客户端Socket的链接
* */
private ServerSocket server;
/**
* 客户端的昵称
* */
private String nickName;
/**
* 线程池,用来控制和管理与客户端交互的线程
* */
private ExecutorService threadPool;
/**
* 该集合用来保存当前服务器中所有客户端的输出流
* 用来实现广播
* */
private List allOut;
/**
* 构造方法,通常用于做初始化操作
* */
public Server(){
try{
//初始化用来存放所有客户端输出流的集合
allOut = new ArrayList();
//初始化ServerSocket
/**
* 向系统申请服务端口8088
* 这样将来外界通过网络发送过来的数据
* 若请求的是8088端口,那么系统就会将这些数据交给我们的程序去处理
* 服务端的端口应该是固定的,都则客户端将无法得知服务端的端口从而导致链接失败
* */
server = new ServerSocket(8088);
//初始化线程池
threadPool = Executors.newFixedThreadPool(50);
}catch(Exception e){
e.printStackTrace();
}
}
/**
*下面三个方法:
*1.向集合中添加输出流
*2.从集合中删除输出流
*3.遍历集合
* 这三个操作既要做到分别同步,又要做到三者互斥,这是因为多线程需要对这个集合做这三个操作,
*对于线程安全的集合而言,自身是可以做到add,remove互斥,并且保证方法同步,
*但是无法做到与遍历进行互斥,所以我们需要自己来维护三个操作的互斥关系
* /
/**
* 将给定的输出流存入共享集合allOut
* */
public synchronized void addOut(PrintWriter pw){
//将给定的流存入集合
allOut.add(pw);
}
/**
* 将给定的输出流从共享集合中删除
* */
public synchronized void removeOut(PrintWriter pw){
//将给定的流从集合中删除
allOut.remove(pw);
}
/**
* 将给定的消息发送给每一个客户端
* */
public synchronized void sendMessageToAllClient(String message){
//遍历集合,将给定的字符串发送给每一个输出流,从而达到广播消息的效果
for(PrintWriter pw:allOut){
pw.println(message);
}
}
/**
* 服务端开始服务的方法
* */
public void start(){
try{
while(true){
/**
* Socket accept()
* ServerSocker的accept方法是一个阻塞方法,调用该方法
* 的目的是监听8088端口,直到一个客户端连接上,这时会返回
* 用于与该客户端交互的Socket
* */
System.out.println("等待客户端链接...");
Socket socket = server.accept();
/**
* 可以通过Socket获取远端客户端的信息,例如:IP地址以及端口号
* */
InetAddress address = socket.getInetAddress();
//获取远端计算机ip
String host = address.getHostAddress();
//获取远端计算机的端口号
int port = socket.getPort();
System.out.println("客户端["+host+":"+port+"]链接了...");
/**
* 当客户端链接后,我们启动一个线程,并将该客户端的Socket传入到线程中,使得
* 让该线程来完成于当前客户端的交互工作,这样我们就可以再次调用accept方法等待
* 其他客户端的链接了
* */
GetClientHandler handler = new GetClientHandler(socket);
// Thread t = new Thread(handler);
// t.start();
/**
* 将任务交给线程池
* */
threadPool.execute(handler);
}
/**
* 服务端这里,通过链接上的客户端的Socket获取输入流
* 来接收该客户端发送过来的数据
* */
// InputStream in = socket.getInputStream();
// InputStreamReader isr = new InputStreamReader(in,"UTF-8");
// BufferedReader br = new BufferedReader(isr);
// /**
// * 读取客户端发送过来的一行字符串
// * */
// String message = null;
//
// if((message = br.readLine())!=null){
// System.out.println("客户端说:"+message);
// }
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args){
Server server = new Server();
server.start();
}
class GetClientHandler implements Runnable{
/**
* 该线程用于交互的客户端的Socket
* */
private Socket socket;
public GetClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
PrintWriter pw = null ;
try{
/**
* 通过Socket获取输出流,用于将消息发送给客户端
* */
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
pw = new PrintWriter(osw,true);
/**
* 将当前线程交互的客户端的输出流存入共享集合,以便可以广播消息给当前客户端
* */
addOut(pw);
/**
* 输出当前在线人数
* 使用存放所有输出流的集合的size就可以作为人数的依据
* */
System.out.println("当前在线人数:"+allOut.size());
/**
* 通过Socket获取输入流
* 然后将输入流转换为缓冲字符输入流
* 循环读取该客户端发送的消息
* */
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(isr);
/**
* 首先读取一个字符串,这个字符串是客户端发送过来的昵称
* */
nickName = br.readLine();
String message = null;
/**
* 这里判断读取的内容是不是null是出于对linux与windows底层实现细节的差异,
* 当客户端与服务器断开链接后,若客户端是Linux用户,那么服务端的br.readLine()方法会
* 返回null,返回null对于缓冲流而言也是表示再没有消息可以读取了,所以就应当停止读取
* 工作了,但是windows的用户端断开链接,则br.readLine()方法会直接抛出异常.
* */
while((message=br.readLine()) != null){
// System.out.println("客户端说:"+message);
// pw.println(message);
/**
* 当该客户端发送一条消息过来后,我们将该消息转发给所有客户端,达到广播的效果
* */
sendMessageToAllClient(nickName+": "+message);
}
}catch(Exception e){
// e.printStackTrace();
}finally{
try{
/**
* 当客户端与服务端断开链接后,先将该客户端的输出流从共享集合中删除停止对该客户端广播消息
* */
removeOut(pw);
System.out.println("一个客户端下线了.");
/**
* 当客户端与服务端断开链接后,
* 服务端这边也应当将Socket关闭,以释放资源.
* */
if(socket!=null){
socket.close();
}
}catch(Exception e2){
e2.printStackTrace();
}
}
}
}
}
Client端
package day05.chat;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端应用程序
* */
public class Client {
/**
* 在客户端运行的Socket,用于链接服务端的ServerSocket
* */
private Socket socket;
/**
* 构造方法,用于初始化客户端
* */
public Client(){
try{
/**
* Socket(String host, int port)
* 创建Socket使用给定的地址以及端口号并发起链接
* */
socket = new Socket("192.168.57.82",8088);
}catch(Exception e){
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
* */
public void start(){
try{
/**
* 启动客户端中用来读取服务端发送消息的线程
* */
GetServerMessageHandler handler = new GetServerMessageHandler();
Thread t = new Thread(handler);
t.start();
/**
* 在客户端,若我们希望向服务端发送数据,
* 我们就通过客户端这边的Socket获取输出流,
* 然后向输出流写出数据就可以了
* */
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
PrintWriter pw = new PrintWriter(osw,true);
//创建一个Scanner用于获取用户输入
Scanner scan = new Scanner(System.in);
//提示用户输入昵称
String nickName = null;
while(true){
System.out.println("请输入昵称:");
nickName = scan.nextLine();
if(nickName.trim().length()<=0){
System.out.println("至少输入一个字符");
}else{
//进行正则表达式验证
break;
}
}
//将昵称发送至服务器
pw.println(nickName);
//输出一个欢迎语
System.out.println("欢迎你!"+nickName+",开始聊天吧!");
/**
* 将字符串输入到服务端
* */
while(true){
pw.println(scan.nextLine());
}
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args){
Client client = new Client();
client.start();
}
/**
* 该线程的作用是用于接受服务端发送过来的信息,
* 并输出到客户端的控制台上
* */
class GetServerMessageHandler implements Runnable{
public void run(){
try{
/**
* 通过Socket获取输入流转换为缓冲字符输入流
* 循环读取服务端发送的消息并输出到控制台
* */
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"UTF-8");
BufferedReader br = new BufferedReader(isr);
String message = null;
while((message=br.readLine())!=null){
System.out.println(message);
}
}catch(Exception e2){
e2.printStackTrace();
}
}
}
}