为了熟练运用BIO, NIO, AIO。先使用BIO实现一个控制台上的多人聊天室。主要的实现方法就是客户端发送消息到服务端,再由服务端进行消息的转发(转发到服务端所监听到的客户端)
目录结构
- client
- ChatClient
- UserInputHandler
- server
- ChatHndler
- ChatServer
client部分
ChatClient
这部分主要是Socket交互功能的实现
public class ChatClient {
private final String DEFAULT_SERVER_HOST = "127.0.0.1";
private final int DEFAULT_SERVER_PORT = 8888;
private final String QUIT = "quit";
private Socket socket;
private BufferedReader reader;
private BufferedWriter writer;
public void sendMessage(String msg) throws IOException {
if (!socket.isOutputShutdown()){
writer.write(msg + "\n");
writer.flush();
}
}
public String receive() throws IOException {
String str = null;
if (!socket.isInputShutdown()){
str = reader.readLine();
}
return str;
}
public boolean readyToQuit(String str){
return str.equals(QUIT);
}
public void close(){
try {
if (writer != null){
System.out.println("server closed");
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
socket = new Socket(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
new Thread(new UserInputHandler(this)).start();
String str = null;
while ((str = receive()) != null){
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
close();
}
}
public static void main(String[] args) {
ChatClient chatClient = new ChatClient();
chatClient.start();
}
}
UserInputHandler
这部分主要是监听键盘输入的线程
public class UserInputHandler implements Runnable{
private ChatClient client;
public UserInputHandler(ChatClient client) {
this.client = client;
}
@Override
public void run() {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while (true){
String input = reader.readLine();
client.sendMessage(input);
if (client.readyToQuit(input)){
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Server部分
ChatServer
这部分主要是实现了Socket交互,使用Map数组存储客户端以及其输入流进行转发消息,在可能存在线程中数据读写时发生错误的方法只是简单的添加了synchronized关键字。另外使用线程池监听客户端,线程池满时阻塞。
public class ChatServer {
private int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private ExecutorService executorService;
private ServerSocket serverSocket;
private Map<Integer, Writer> connectedClients;
public ChatServer(){
executorService = Executors.newFixedThreadPool(10);
connectedClients = new HashMap<>();
}
/**
* 添加客户端
* @param socket
* @throws IOException
*/
public synchronized void addClient(Socket socket) throws IOException {
if (socket != null){
int port = socket.getPort();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
connectedClients.put(port, writer);
System.out.println("client[" + port +"]connected server");
}
}
/**
* 客户端关闭
* @param socket
* @throws IOException
*/
public synchronized void removeClient(Socket socket) throws IOException {
if (socket != null){
int port = socket.getPort();
if (connectedClients.containsKey(port)){
connectedClients.get(port).close();
}
connectedClients.remove(port);
System.out.println("client[" + port +"]connect finished");
}
}
/**
* 转发消息
* @param socket
* @param fwdMsg
* @throws IOException
*/
public synchronized void forwardMessage(Socket socket, String fwdMsg) throws IOException {
for (Integer id: connectedClients.keySet()){
if (!id.equals(socket.getPort())){
Writer writer = connectedClients.get(id);
writer.write(fwdMsg);
}
}
}
public boolean readyToQuit(String msg){
return QUIT.equals(msg);
}
private synchronized void close(){
try {
if (serverSocket != null){
serverSocket.close();
System.out.println("serverSocket closed");
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
// 登台监听端口
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("server start, listen: " + DEFAULT_PORT + "...");
while (true){
// 等待客户端连接
Socket socket = serverSocket.accept();
// 不用没进来一个用户都要单独创建一个线程,浪费资源
// new Thread(new ChatHandler(this, socket)).start();
executorService.execute(new ChatHandler(this, socket));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
close();
}
}
public static void main(String[] args) {
ChatServer server = new ChatServer();
server.start();
}
}
ChatHandler
这部分线程主要实现数据的接收以及转发功能。
public class ChatHandler implements Runnable{
private ChatServer chatServer;
private Socket socket;
public ChatHandler(ChatServer chatServer, Socket socket) {
this.chatServer = chatServer;
this.socket = socket;
}
@Override
public void run() {
try {
chatServer.addClient(socket);
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
String msg = null;
while ((msg = reader.readLine()) != null){
if (chatServer.readyToQuit(msg)){
break;
}
String fwdMsg = "client[" + socket.getPort() + "]: " + msg + "\n";
System.out.println(fwdMsg);
chatServer.forwardMessage(socket, fwdMsg);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
System.out.println("client[" + socket.getPort() +"] closed");
chatServer.removeClient(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}