前言
上一篇文章,运用 socket 和 多线程 实现了在 IDE 控制台一对一聊天的功能,本文将更近一步,实现客户端和服务器端自由交互式聊天,不再限制一次一条的限制
使用线程解决IO读写阻塞的问题
虽然 socket 是全双工传输模式,但是由于读写IO 是两个过程,所以,如果想要实现客户端和服务器端自由发送接收信息,就需要将服务器端和客户端发送数据和读取数据的过程分开,即使用多线程。
总体思路
服务器端使用 socket
服务器端使用线程池
服务器端读客户端消息和返回客户端消息各自使用一个线程
客户端使用线程池
客户端写和读各自使用一个线程
代码
服务器端和客户端都是通过运行 main 方法启动
服务器端
注意编码格式为 UTF8
服务端使用了线程池
需要在感知到客户端关闭后主动回收资源
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* socket 服务端
* 可以和客户端进行自由交互
*
* @Author: jie.wu
*/
public class FreedomServer {
//线程池
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
private final static String charset = "UTF8";
public static void main(String[] args) {
//服务端
ServerSocket serverSocket;
try {
serverSocket = new ServerSocket(10005);
System.out.println("服务端已启动,您本地的字符格式为" + Charset.defaultCharset().name() + ",等待客户端访问");
//等待客户端的访问
for (; ; ) {
//客户端请求
Socket clientSocket;
try {
clientSocket = serverSocket.accept();
executorService.submit(new Server(clientSocket));
} catch (IOException e) {
System.out.println("报错了2" + e);
}
}
} catch (IOException e) {
System.out.println("报错了1" + e);
}
}
//服务器处理客户端请求的线程
public static class Server implements Runnable {
private Socket clientSocket;
public Server(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
//开启一个读线程
executorService.submit(new ReadThread(clientSocket));
//开启一个写线程
executorService.submit(new WriteThread(clientSocket));
}
}
//接收信息的线程
public static class ReadThread implements Runnable {
private Socket clientSocket;
private InputStream inputStream;
private BufferedReader bufferedReader;
public ReadThread(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
//如果客户端没有关闭写,则持续读取
while (true) {
try {
inputStream = clientSocket.getInputStream();
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, charset));
String str;
for (; (str = bufferedReader.readLine()) != null; ) {
//System.out.println("客户端:"+str);
System.out.println(clientSocket.getInetAddress().getHostAddress() + ":" + str);
}
} catch (Exception e) {
System.out.println("报错了3,可能是客户端由于某种原因断开了连接" + e + ";" + Thread.currentThread().getName());
break;//结束本次对客户端的循环
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (bufferedReader != null) {
bufferedReader.close();
}
if (clientSocket != null) {
clientSocket.close();
}
} catch (Exception e) {
System.out.println("报错了" + 5);
}
}
}
}
}
//发送信息的线程
public static class WriteThread implements Runnable {
private Socket clientSocket;
private OutputStream outputStream;
private PrintWriter printWriter;
public WriteThread(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
Scanner s = new Scanner(System.in);
try {
outputStream = clientSocket.getOutputStream();
printWriter = new PrintWriter(new OutputStreamWriter(outputStream, charset), true);
String str = "";
// while (s.hasNext() && (str = s.nextLine()) != null) {
// //System.out.print("服务端:");
// printWriter.println(str);
// }
for (;s.hasNext() && (str = s.nextLine()) != null;) {
//System.out.print("服务端:");
printWriter.println(str);
}
} catch (Exception e) {
System.out.println("报错了4,可能是客户端由于某种原因断开了连接" + e);
} finally {
try {
if (printWriter != null) {
printWriter.close();
}
if (outputStream != null) {
outputStream.close();
}
if (clientSocket != null) {
clientSocket.close();
}
} catch (Exception e) {
System.out.println("报错了" + 5);
}
}
}
}
}
客户端
注意编码格式为 UTF8
需要注意的是,客户端需要在感知服务器端关闭后结束自己读和写中死循环的逻辑
读线程会主动感知
写线程需要在再次发送信息读时候感知
客户端可以不使用线程池,如果使用线程池,需要使用线程池需要主动关闭线程池,否则在服务器关闭后连接无法断开
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* socket 客户端
* 可以和服务器端进行自由交互
* @Author: jie.wu
*/
public class FreedomClient {
//线程池
private static ExecutorService executorService=Executors.newFixedThreadPool(10);
private final static String charset="UTF8";
public static void main(String [] args){
try{
Socket socket=new Socket();
socket.connect(new InetSocketAddress("127.0.0.1", 10000));
System.out.println("客户端已启动,您本地的字符格式为"+Charset.defaultCharset().name()+",请输入信息:");
executorService.submit(new ReadThread(socket));
executorService.submit(new WriteThread(socket));
executorService.shutdown();//在子线程运行结束后关闭线程池,否则主进程不会结束,感觉测试不完整
//由于客户端只有两个进程,所有可以不使用线程池
// Thread t1=new Thread(new ReadThread(socket));
// Thread t2=new Thread(new WriteThread(socket));
// t1.start();
// t2.start();
// t1.join();
// t2.join();
// System.out.println("结束了--");
}catch(Exception e){
System.out.println("报错了1"+e);
}finally {
}
}
//发送消息的线程
public static class WriteThread implements Runnable{
private Socket clientSocket;
private OutputStream outputStream;
private PrintWriter printWriter;
public WriteThread(Socket clientSocket){
this.clientSocket=clientSocket;
}
@Override
public void run() {
Scanner s=new Scanner(System.in);
while(true){
try{
outputStream =clientSocket.getOutputStream();
printWriter=new PrintWriter(new OutputStreamWriter(outputStream,charset),true);
//方式1
printWriter.println(s.nextLine());
/**
//方式2
String str = "";
for (; (str = s.nextLine()) != null; ) {
//System.out.println("客户端:"+str);
printWriter.println(str);
}
*/
/*
//方式3
String str = "";
while(s.hasNext()){
str = s.nextLine();
System.out.println("服务端:"+str);
printWriter.println(str);
}
*/
}catch(Exception e){
System.out.println("报错了4,可能是服务端由于某种原因断开了连接,不再向服务器发送信息"+e);
try{
if(printWriter!=null){ printWriter.close();}
if(outputStream!=null){ outputStream.close();}
if(clientSocket!=null){ clientSocket.close();}
break;//结束本次对客户端的循环
}catch(Exception e2){
System.out.println("报错了5"+e2);
}
}
}
}
}
//接收消息的线程
public static class ReadThread implements Runnable{
private Socket clientSocket;
private InputStream inputStream;
private BufferedReader bufferedReader;
public ReadThread(Socket clientSocket){
this.clientSocket=clientSocket;
}
@Override
public void run() {
//如果客户端没有关闭写,则持续读取
while(true){
try{
inputStream=clientSocket.getInputStream();
bufferedReader=new BufferedReader(new InputStreamReader(inputStream,charset));
String str;
for(;(str=bufferedReader.readLine())!=null;){
System.out.println("服务端:"+str);
}
}catch(Exception e) {
System.out.println("报错了3,可能是服务端由于某种原因断开了连接,停止接收服务器信息,请输入任意字符结束访问" + e + ";" + Thread.currentThread().getName());
break;//结束本次对客户端的循环
}finally{
System.out.println("ReadThread finally");
try{
if(inputStream!=null){ inputStream.close();}
if(bufferedReader!=null){ bufferedReader.close();}
if(clientSocket!=null){ clientSocket.close();}
}catch(Exception e){
System.out.println("报错了"+5);
}
}
}
}
}
}
运行效果
服务端
客户端
关于资源回收
由于客户端和服务器端的交互次数是没有限定的,所以我们需要捕获一个异常作为交互结束的的标识,这个异常就是客户端或者服务器端断开连接,捕获到这个异常后就需要 break ,中断死循环,然后在 finally 代码块中进行资源回收。