聊天室实现——使用Socket 和 ServerSocket实现聊天小项目
Posted Johnny*
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊天室实现——使用Socket 和 ServerSocket实现聊天小项目相关的知识,希望对你有一定的参考价值。
ServerSocket API
构造方法
ServerSocket() //创建一个没有绑定端口的服务器套接字
ServerSocket(int port) //创建一个绑定端口(port)的套接字
常用API
SocketAddress getLocalSocketAddress() // 返回此socket绑定的端点的地址
Socket accept() //侦听并接收连接到本服务的客户端连接
多线程
建立固定线程个数的线程池
//创建拥有10个线程的线程池:当客户端申请连接时,便为之分配一个线程
ExecutorService executorService = Executors.newFixedThreadPool(10);
newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程。任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子
Java ExecutorService
Socket
构造方法
//
Socket()
Socket(String host,int port) //创建套接字并连接到指定Ip和端口号的远程服务器端
Socket(InetAddress address, int port) //创建一个流套接字并将其连接到指定主机上的指定端口号。
InputStream getInputStream() //返回该socket的输入流
OutputStream getOutputStream() //返回该socket的输出流
聊天小项目
服务端功能实现
- 维护功能:维护所有在线的客户端
- 注册功能:将客户端的名称添加到服务器的客户端集合(ONLINE_CLIENT_MAP,该Map维护一个<String userName,Socket client> 键值对)
- 私聊功能:客户端与指定客户端发送和接受数据
- 退出功能: 从服务器客户端集合移除客户端
客户端功能实现
- 注册功能: 创建Socket,给服务端发送注册执行
- 私聊功能: 客户端指定客户端发送数据,相应客户端接收数据
- 退出功能: 给服务器发送退出指令
实现思路
**第一步:**服务端要开启连接功能,并处于监听状态。客户端只需根据服务端的端口和IP地址即可发送连接的请求。
**第二步:**由于是多人聊天因此使用多线程奇数。开辟固定大小的线程池来控制客户端连接数量。
MulThreadServerHandler
package com.johnny.chatroom.web.controller;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 多线程服务器端
*/
public class MulThreadServerHandler {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
//1. 启动服务端
serverSocket = new ServerSocket(2158);
System.out.println("服务器启动……"+serverSocket.getLocalSocketAddress());
//2. 创建线程池:当客户端申请连接时,便为之分配一个线程
ExecutorService executorService = Executors.newFixedThreadPool(10);
//3. 阻塞,侦听并接收连接到本服务的客户端连接
while(true){
Socket client = serverSocket.accept();
System.out.println("有客户端连接到服务器:"+client.getRemoteSocketAddress());
executorService.execute(new ClientHandler(client));
}
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
第三步: 服务端建立好连接之后,就需等待客户端连接。客户端也不单单是连接了服务器就行了,还有信息的交互,包括从服务端读取数据(ReadDataFromServer)和发送数据给服务端(WriteDataToServerThread)。由于读写两个过程可以同时进行,因此开启两个线程来实现。
发送数据给服务端
package com.johnny.chatroom.web.controller;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;
public class WriteDataToServerThread extends Thread{
private final Socket client;
public WriteDataToServerThread(Socket client){
this.client = client;
}
@Override
public void run() {
OutputStream clientOutput = null;
OutputStreamWriter writer = null;
Scanner reader = null;
try {
clientOutput = client.getOutputStream();
writer = new OutputStreamWriter(clientOutput);
reader = new Scanner(System.in);
while(true){
System.out.println("输入信息(回车结束!):");
String msg = reader.nextLine();
writer.write(msg+"\\n");
writer.flush();
if(msg.equals("bye")){
// 表示客户端要关闭
client.close();
break; //没有这个,bye时,read会有异常,因为已知在while里等待输入
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(reader != null ) reader.close();
try {
if(clientOutput != null) clientOutput.close();
if(writer != null ) writer.close();
} catch (IOException e) { e.printStackTrace(); }
}
}
}
从服务端读取数据
package com.johnny.chatroom.web.controller;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.Scanner;
public class ReadDataFromServerThread extends Thread{
private final Socket client;
public ReadDataFromServerThread(Socket client) {
this.client = client;
}
@Override
public void run() {
InputStream clientInput = null;
try {
clientInput = client.getInputStream();
Scanner scanner = new Scanner(clientInput);
while(true){
String data = scanner.nextLine();
System.out.println("来自服务器的消息:"+data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
第四步: 实现具体服务器客户端业务逻辑
package com.johnny.chatroom.web.controller;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class ClientHandler implements Runnable{
//在线用户Map final 且为concurrentHashMap(线程安全)
private static final Map<String, Socket> ONLINE_CLIENT_MAP = new ConcurrentHashMap<>();
//单例模式
private final Socket client;
public ClientHandler(Socket client){
this.client = client;
}
@Override
public void run() {
try {
InputStream clientInput = client.getInputStream();
Scanner scanner = new Scanner(clientInput);
while (true){
String data = scanner.nextLine();
//注册
if( data.startsWith("register:")){
String userName = data.split(":")[1];
register(userName);
continue;
}
//私聊
if(data.startsWith("privateChat:")){
String[] segments = data.split(":");
String targetUserName = segments[1];
String message = segments[2];
privateChat(targetUserName,message);
continue;
}
//退出
if(data.equals("bye")){
bye();
continue;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端私聊
* 往指定targetUserName发送消息
* @param targetUserName
* @param message
*/
private void privateChat(String targetUserName, String message) {
//获取目标客户socket
Socket target = ONLINE_CLIENT_MAP.get(targetUserName);
if( target == null)
this.sendMessage(this.client,"用户不存在", false);
else{
String currentUserName = this.getCurrentUserName();
this.sendMessage(target,message,true);
}
}
/**
* 注册过程就是往ONLINE_CLIENT_MAP添加<String userName, Socket><
* @param userName
*/
private void register(String userName) {
ONLINE_CLIENT_MAP.put(userName, this.client);
this.sendMessage(this.client, "恭喜"+userName+"注册成功", false);
}
/**
* 往指定客户端(target)发送消息
* @param target
* @param message
* @param prefix
*/
private void sendMessage(Socket target, String message, boolean prefix) {
String currentUserName = this.getCurrentUserName();
OutputStream clientOutput = null;
try {
clientOutput = target.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(clientOutput);
if(prefix){
writer.write(""+currentUserName+":"+message+"\\n");
}else{
writer.write(message+"\\n");
}
//刷出缓存区 才能在控制台显示
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}//通讯时双方的IO都要一直打开着
}
/**
* 返回用户名字
* @return
*/
private String getCurrentUserName() {
Set<Map.Entry<String, Socket>> clientSet = ONLINE_CLIENT_MAP.entrySet();
for( Map.Entry<String, Socket> entry: clientSet){
if(entry.getValue().equals(this.client) ){
return entry.getKey();
}
}
return null;
}
/**
* 退出
*/
public void bye(){
Set<Map.Entry<String, Socket>> clientSet = ONLINE_CLIENT_MAP.entrySet();
for( Map.Entry<String, Socket> entry: clientSet){
if(entry.getValue().equals(this.client) ){
ONLINE_CLIENT_MAP.remove(entry.getKey());
break;
}
}
}
}
测试
开启两个进程测试
package com.johnny.chatroom.web.test;
import com.johnny.chatroom.web.controller.ReadDataFromServerThread;
import com.johnny.chatroom.web.controller.WriteDataToServerThread;
import java.io.IOException;
import java.net.Socket;
public class ClientThread1 {
public static void main(String[] args) {
try {
Socket client = new Socket("0.0.0.0", 2158);
//开启读写进程
new WriteDataToServerThread(client).start();
new ReadDataFromServerThread(client).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.johnny.chatroom.web.test;
import com.johnny.chatroom.web.controller.ClientHandler;
import com.johnny.chatroom.web.controller.ReadDataFromServerThread;
import com.johnny.chatroom.web.controller.WriteDataToServerThread;
import java.io.IOException;
import java.net.Socket;
public class ClientThread2 {
public static void main(String[] args) {
try {
Socket client = new Socket("0.0.0.0", 2158);
ClientHandler clientHandler = new ClientHandler(client);
//开启读写进程
new WriteDataToServerThread(client).start();
new ReadDataFromServerThread(client).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
大致流程:
执行MulThreadServerHandler:1、启动服务端,等待客户端的连接申请。serverSocket.accept()会阻塞,侦听并接收连接到本服务的客户端连接。
执行ClientThread1: 客户端向服务发起请求连接。
服务端接收到客户端的连接请求之后, serverSocket.accept()拿到客户端的Socket对象。
之后执行executorService.execute(new ClientHandler(client)),为该socket对象分配一个线程,该线程有ClientHandler对象。
error
异常:java.util.NoSuchElementException: No line found
可能是错误地关闭了Scanner reader
以上是关于聊天室实现——使用Socket 和 ServerSocket实现聊天小项目的主要内容,如果未能解决你的问题,请参考以下文章