多线程及TCP通信 实例之 聊天室
使用Java的Socket实现客户端和服务端之间的连接,并使得客户端向服务端发送一条消息。
步骤:
1、 创建客户端类
/**
* 客户端应用程序
*
* @author Administrator
*
*/
public class Client {
....
}
2、 创建Socket对象
// Socket 用于连接服务器的ServerSocket
private Socket socket;
/**
* 客户端构造方法,用于初始化客户端
*/
public Client() throws Exception {
try {
/**
* 创建Socket对象时,就会尝试根据给定的 地址与端口连接服务端。 所以,若该对象创建成功,说明与服务端连接正常。
*/
System.out.println("正在连接服务器。。。");
socket = new Socket(
"localhost", 8088);
} catch (Exception e) {
throw e;
}
}
3、创建客户端方法 start()
/**
* 客户端启动方法
*/
public void start() {
try {
//创建并启动线程,来接收服务端的信息
Runnable runn=new GetServerInfoHandler();
Thread t=new Thread(runn);
t.start();
OutputStream out = socket.getOutputStream();
/**
* 使用字符流来格局指定的编码集将字符串转换为字节后 在通过out发送给服务器端
*/
OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");
/**
* 将字符流包装为缓冲字符流,就可 以按行为单位写出字符串了
*/
PrintWriter pw = new PrintWriter(osw,true);
/**
* 创建一个Scanner,用于接收
* 用户的输入的字符串
*/
Scanner scanner = new Scanner(System.in);
//输出欢迎用语
System.out.println("欢迎来到小邓的聊天室");
while(true){
/**
* 首先输入昵称
*/
System.out.println("请输入昵称:");
String nickname=scanner.nextLine();
if(nickname.trim().length()>0){
pw.println(nickname);
break;
}
System.out.println("昵称不能为空!");
}
//循环读取并输出用户输入的内容
while (true) {
String str = scanner.nextLine();
pw.println(str);
}
} catch (Exception e) {
e.printStackTrace();
}
}
4、为客户端类定义main()方法
public static void main(String[] args) {
try {
Client client = new Client();
client.start();
} catch (Exception e) {
e.printStackTrace();
System.out.println("客户端初始化失败!");
}
}
/**
* 该线程的作用是循环接收服务端发送来的信息,并
* 输出到控制台来
* @author Administrator
*
*/
5、创建线程实现字符串的输出
class GetServerInfoHandler 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 e){
}
}
}
6、 创建服务器端类
/**
* 服务端应用程序
*
* @author Administrator
*
*/
public class Server {
....
}
7、 创建ServerSocket类的对象,构造方法,创建线程池,集合。
// 运行在服务端的Socket
private ServerSocket server;
// 线程池,用于管理客户端连接的交互线程
private ExecutorService threadPool;
// 保存所有客户端输出流的集合
private List<PrintWriter> allOut;
/**
* 构造方法,用于初始化服务端
*/
public Server() {
try {
System.out.println("初始化服务端");
server = new ServerSocket(8088);
// 初始化线程池
threadPool = Executors.newFixedThreadPool(50);
// 初始化存放所有客户端输出流的集合
allOut = new ArrayList<PrintWriter>();
System.out.println("服务端初始化完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
8、创建服务端工作方法 start()
/**
* 服务端开始工作的方法
*/
public void start() {
try {
while (true) {
System.out.println("等待客户端连接。。。");
// ServerSocket的accept方法
/**
* 用于监听8088端口,等待客户端的连接 该方法是阻塞方法,直到一个客户端连接,否则发I方法一直紫塞
* 若一个客户端连接了,会返回该客户端的Socket
*/
Socket socket = server.accept();
/**
* 当一个客户端连接后,启动一个线程ClientHandler,将 该客户端的Socket传入,使得该线程处理与该客户端的交互
* 这样,我们能再次进入循环,接收下一个客户端的连接了
*/
Runnable handler = new ClientHandler(socket);
// Thread t=new Thread(handler);
/**
* 使用线程池分配空闲线程来处理 当前连接的客户端
*/
threadPool.execute(handler);
// t.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
9、 为服务端类定义main 方法
public static void main(String[] args) {
Server server;
server = new Server();
server.start();
}
10、实现消息通信及转发
/**
* 将给定的输出流存入共享集合 加synchronized锁,使异步操作变成同步操作
*
* 在不同一方法加上synchronized之后,变成互斥锁 只能执行一个方法,此时可保存线程安全
*/
public synchronized void addOut(PrintWriter pw) {
allOut.add(pw);
}
/**
* 将给定的输出流从共享集合中删除 加synchronized锁,使异步操作变成同步操作
*
* @param pw
*/
public synchronized void removeOut(PrintWriter pw) {
allOut.remove(pw);
}
/**
* 将给定的消息转发给所有客户端 加synchronized锁,使异步操作变成同步操作
*
* @param message
*/
public synchronized void sendMessage(String message) {
for (PrintWriter pw : allOut) {
pw.println(message);
}
}
/**
* 服务端中的一个线程,用于与某个客户交互 使用线程的目的是使得服务端可以处理多个客户端了。
*
* @author Administrator
*
*/
// 创建线程
class ClientHandler implements Runnable {
// 当前线程处理的客户端的Socket
private Socket socket;
// 当前客户端的IP
private String ip;
// 当前用户的昵称
private String nickname;
/**
* 根据给定的客户端的Socket,创建线程体
*
* @param socket
*/
public ClientHandler(Socket socket) {
this.socket = socket;
// 获取远端的的地址
InetAddress address = socket.getInetAddress();
// 获取本端的地址信息
// socket.getLocalAddress();
// 获取远端的计算机ip地址
ip = address.getHostAddress();
// 获取客户端的端口号
int port = socket.getPort();
}
/**
* 该线程会将Socket中的输入流获取 用来循环读取客户端发送过来的消息
*/
public void run() {
/**
* 定义在try语句外的目的是,为了在 finally中也可以引用到
*/
PrintWriter pw = null;
try {
/**
* 为了让服务端与客户端发送信息, 我们需要通过socket获取输出流
*/
OutputStream out = socket.getOutputStream();
// 转换为字符流,用于指定编码集
OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");
// 创建缓冲字符输出流
pw = new PrintWriter(osw, true);
/**
* 将该客户端的输出流存入共享集合以便 使得该客户端也能接收服务端转发的 消息
*/
// 存在线程安全问题,不推荐
// allOut.add(pw);
addOut(pw);
// 是输出在线人数
System.out.println("当前在线人数为:" + allOut.size());
// System.out.println("客户端连接成功");
/**
* 通过刚刚连上的客户端的Socket获取输入流 读取客户端发送的· 数据
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, "utf-8");
/**
* 将字符流转换为缓冲字符输入流 这样就可以以行为单位读取字符串了
*/
BufferedReader br = new BufferedReader(isr);
/**
* 当创建好当前客户端的输入流后 读取的第一个字符串,应当是昵称
*
*/
nickname = br.readLine();
// 通知所有客户端,当前用户上线了
sendMessage("[" + nickname + "]上线了");
// 读取客户端发过来的字符串
/**
* windows与linux存在一定的差异: linux:当客户端与服务端断开连接后
* 我们通过输入流会读到null但这是合乎逻辑的,因为缓冲流的
* readLine()方法若返回null就表示无法通过该流再读取到 信息。参考之前服务文本文件的判断
* windows:当客户端与服务端断开连接后 readLine()方法会抛出异常
*
*/
String message = null;
while ((message = br.readLine()) != null) {
//pw.println(message);
/**
* 当读取到客户端发送过来的一条消息后, 将该消息装发给所有客户端
*/
// 存在线程安全问题,遍历过程中不允许有删改
// for(PrintWriter o:allOut){
// o.println(message);
// }
sendMessage(nickname + "说:" + message);
}
} catch (Exception e) {
// 在windows中的客户端,报错通常是因为客户端断开了连接
// 通知其他用户,该用户下线了
sendMessage("[" + nickname + "]下线了");
} finally {
/**
* 首先将该客户端的输出流从共享 集合中删除。
*/
// 存在现存安全问题
// allOut.remove(pw);
removeOut(pw);
// 输出当前在线人数
System.out.println("当前在线人数为:" + allOut.size());
// 通知其他用户,该用户下线了
sendMessage("[" + nickname + "]下线了");
/**
* 无论Linux还是windows用户,当与服务器断开连接后 我们都应该在服务器断开与客户端断开连接
*/
try {
socket.close();
} catch (IOException e) {
System.out.println("一个客户下线了。。。");
}
}
}
}
11、完整代码
Client类
package day06;
import java.io.BufferedReader;
import java.io.IOException;
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.net.UnknownHostException;
import java.util.Scanner;
/**
* 客户端应用程序
*
* @author Administrator
*
*/
public class Client {
// Socket 用于连接服务器的ServerSocket
private Socket socket;
/**
* 客户端构造方法,用于初始化客户端
*/
public Client() throws Exception {
try {
/**
* 创建Socket对象时,就会尝试根据给定的 地址与端口连接服务端。 所以,若该对象创建成功,说明与服务端连接正常。
*/
System.out.println("正在连接服务器。。。");
socket = new Socket(
"localhost", 8088);
} catch (Exception e) {
throw e;
}
}
/**
* 客户端启动方法
*/
public void start() {
try {
//创建并启动线程,来接收服务端的信息
Runnable runn=new GetServerInfoHandler();
Thread t=new Thread(runn);
t.start();
OutputStream out = socket.getOutputStream();
/**
* 使用字符流来格局指定的编码集将字符串转换为字节后 在通过out发送给服务器端
*/
OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");
/**
* 将字符流包装为缓冲字符流,就可 以按行为单位写出字符串了
*/
PrintWriter pw = new PrintWriter(osw,true);
/**
* 创建一个Scanner,用于接收
* 用户的输入的字符串
*/
Scanner scanner = new Scanner(System.in);
//输出欢迎用语
System.out.println("欢迎来到小邓的聊天室");
while(true){
/**
* 首先输入昵称
*/
System.out.println("请输入昵称:");
String nickname=scanner.nextLine();
if(nickname.trim().length()>0){
pw.println(nickname);
break;
}
System.out.println("昵称不能为空!");
}
//循环读取并输出用户输入的内容
while (true) {
String str = scanner.nextLine();
pw.println(str);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
Client client = new Client();
client.start();
} catch (Exception e) {
e.printStackTrace();
System.out.println("客户端初始化失败!");
}
}
/**
* 该线程的作用是循环接收服务端发送来的信息,并
* 输出到控制台来
* @author Administrator
*
*/
class GetServerInfoHandler 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 e){
}
}
}
}
Server类
package day06;
import java.io.BufferedReader;
import java.io.IOException;
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;
/**
* 服务端应用程序
*
* @author Administrator
*
*/
public class Server {
// 运行在服务端的Socket
private ServerSocket server;
// 线程池,用于管理客户端连接的交互线程
private ExecutorService threadPool;
// 保存所有客户端输出流的集合
private List<PrintWriter> allOut;
/**
* 构造方法,用于初始化服务端
*/
public Server() {
try {
System.out.println("初始化服务端");
server = new ServerSocket(8088);
// 初始化线程池
threadPool = Executors.newFixedThreadPool(50);
// 初始化存放所有客户端输出流的集合
allOut = new ArrayList<PrintWriter>();
System.out.println("服务端初始化完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start() {
try {
while (true) {
System.out.println("等待客户端连接。。。");
// ServerSocket的accept方法
/**
* 用于监听8088端口,等待客户端的连接 该方法是阻塞方法,直到一个客户端连接,否则发I方法一直紫塞
* 若一个客户端连接了,会返回该客户端的Socket
*/
Socket socket = server.accept();
/**
* 当一个客户端连接后,启动一个线程ClientHandler,将 该客户端的Socket传入,使得该线程处理与该客户端的交互
* 这样,我们能再次进入循环,接收下一个客户端的连接了
*/
Runnable handler = new ClientHandler(socket);
// Thread t=new Thread(handler);
/**
* 使用线程池分配空闲线程来处理 当前连接的客户端
*/
threadPool.execute(handler);
// t.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 将给定的输出流存入共享集合 加synchronized锁,使异步操作变成同步操作
*
* 在不同一方法加上synchronized之后,变成互斥锁 只能执行一个方法,此时可保存线程安全
*/
public synchronized void addOut(PrintWriter pw) {
allOut.add(pw);
}
/**
* 将给定的输出流从共享集合中删除 加synchronized锁,使异步操作变成同步操作
*
* @param pw
*/
public synchronized void removeOut(PrintWriter pw) {
allOut.remove(pw);
}
/**
* 将给定的消息转发给所有客户端 加synchronized锁,使异步操作变成同步操作
*
* @param message
*/
public synchronized void sendMessage(String message) {
for (PrintWriter pw : allOut) {
pw.println(message);
}
}
public static void main(String[] args) {
Server server;
server = new Server();
server.start();
}
/**
* 服务端中的一个线程,用于与某个客户交互 使用线程的目的是使得服务端可以处理多个客户端了。
*
* @author Administrator
*
*/
// 创建线程
class ClientHandler implements Runnable {
// 当前线程处理的客户端的Socket
private Socket socket;
// 当前客户端的IP
private String ip;
// 当前用户的昵称
private String nickname;
/**
* 根据给定的客户端的Socket,创建线程体
*
* @param socket
*/
public ClientHandler(Socket socket) {
this.socket = socket;
// 获取远端的的地址
InetAddress address = socket.getInetAddress();
// 获取本端的地址信息
// socket.getLocalAddress();
// 获取远端的计算机ip地址
ip = address.getHostAddress();
// 获取客户端的端口号
int port = socket.getPort();
}
/**
* 该线程会将Socket中的输入流获取 用来循环读取客户端发送过来的消息
*/
public void run() {
/**
* 定义在try语句外的目的是,为了在 finally中也可以引用到
*/
PrintWriter pw = null;
try {
/**
* 为了让服务端与客户端发送信息, 我们需要通过socket获取输出流
*/
OutputStream out = socket.getOutputStream();
// 转换为字符流,用于指定编码集
OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");
// 创建缓冲字符输出流
pw = new PrintWriter(osw, true);
/**
* 将该客户端的输出流存入共享集合以便 使得该客户端也能接收服务端转发的 消息
*/
// 存在线程安全问题,不推荐
// allOut.add(pw);
addOut(pw);
// 是输出在线人数
System.out.println("当前在线人数为:" + allOut.size());
// System.out.println("客户端连接成功");
/**
* 通过刚刚连上的客户端的Socket获取输入流 读取客户端发送的· 数据
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, "utf-8");
/**
* 将字符流转换为缓冲字符输入流 这样就可以以行为单位读取字符串了
*/
BufferedReader br = new BufferedReader(isr);
/**
* 当创建好当前客户端的输入流后 读取的第一个字符串,应当是昵称
*
*/
nickname = br.readLine();
// 通知所有客户端,当前用户上线了
sendMessage("[" + nickname + "]上线了");
// 读取客户端发过来的字符串
/**
* windows与linux存在一定的差异: linux:当客户端与服务端断开连接后
* 我们通过输入流会读到null但这是合乎逻辑的,因为缓冲流的
* readLine()方法若返回null就表示无法通过该流再读取到 信息。参考之前服务文本文件的判断
* windows:当客户端与服务端断开连接后 readLine()方法会抛出异常
*
*/
String message = null;
while ((message = br.readLine()) != null) {
//pw.println(message);
/**
* 当读取到客户端发送过来的一条消息后, 将该消息装发给所有客户端
*/
// 存在线程安全问题,遍历过程中不允许有删改
// for(PrintWriter o:allOut){
// o.println(message);
// }
sendMessage(nickname + "说:" + message);
}
} catch (Exception e) {
// 在windows中的客户端,报错通常是因为客户端断开了连接
// 通知其他用户,该用户下线了
sendMessage("[" + nickname + "]下线了");
} finally {
/**
* 首先将该客户端的输出流从共享 集合中删除。
*/
// 存在现存安全问题
// allOut.remove(pw);
removeOut(pw);
// 输出当前在线人数
System.out.println("当前在线人数为:" + allOut.size());
// 通知其他用户,该用户下线了
sendMessage("[" + nickname + "]下线了");
/**
* 无论Linux还是windows用户,当与服务器断开连接后 我们都应该在服务器断开与客户端断开连接
*/
try {
socket.close();
} catch (IOException e) {
System.out.println("一个客户下线了。。。");
}
}
}
}
}
12、测试
- 服务端运行
- 客户端运行
提示请输入昵称,若不输入昵称,提示昵称为空,强制输入。
客户端显示在线人数
-消息通信
其他客户端进行转发
下线通知