[TOC]
前言
- 环境:openjdk8
- 参考书籍:java多线程编程实战指南(核心篇) 黄文海
- 注意:这个主要是试用下BIO通讯,重点是使用BIO,不是实现聊天室,所以写的很糙,确实很丑......
分析
单线程版
特点:
- 阻塞
- 一对一
监听到一个client连接,只创建一个Socket线程,与之进行交互
同样,这里的交互也是阻塞的
可以是
- 先读后写
- 先写后读
注意:客户端与服务端口不可同时读/同时写,以免造成死锁
这样的读写显然是无法满足需要的,聊天回合制可还行
可以另开一个线程完成读/写,达到异步的效果
(当然,这个理想情况下的图,读写并行)
依此,可以实现一个简单的,服务器与客户端一对一的交流
但是,这也是不满足需求的,哪有服务器和客户端聊天的,应为客户端对客户端.所以,
- 每接受到一个客户端链接,就另开一个线程与之通信
- 服务端收到客户端信息,将信息发往与之有关联的客户端线程
Server的读写就不需要分离了,收到信息后就发送给关联的client就行
这里只是简单实现,所以就不搞分组,都在一个组内好了.如何管理聊天呢?需要保证每个client都可以收到组内成员发的信息,我的想法是定一个线程共享的集合来存储创建的socket,每收到信息,就遍历socket集合,发送信息给每一个client.所以思路如下
分析总结
server层:
- 循环监听连接
- 每监听到一个连接,创建socket,将socket存储到线程共享且线程安全的socket集合中,另开一线程与client进行通信
- 读写一个线程,先读后写,每收到一个信息,就遍历socket集合,给每一个client发送信息
client层:
- 连接server
- 连接到后,进行通信,通信循环,直到达到循环不成立的条件
- 读写分离,各占一个线程
实现
server
初始化
设置监听队列长度,ip地址与端口.这个给个端口就行了,其它使用默认值好了
看了看源码,
public ServerSocket(int port) throws IOException {
this(port, 50, null);
}
默认监听队列长是50,也就是监听50个连接请求,这个没关系,监听是监听,处理是处理.
跳一跳,看看默认的ip地址是啥
public synchronized InetAddress anyLocalAddress() {
if (anyLocalAddress == null) {
anyLocalAddress = new Inet4Address(); // {0x00,0x00,0x00,0x00}
anyLocalAddress.holder().hostName = "0.0.0.0";
}
return anyLocalAddress;
}
扒到了,默认ip地址是0.0.0.0,也就是本机所有ip都可以被client访问.我一直以为默认的ip地址是127.0.0.1
好了,没啥说的了,看初始化代码如下:
private final static int PORT = 8080;
private static ServerSocket ss;
private static void init() {
try {
ss = new ServerSocket(PORT);
Logger.getGlobal().info("server 初始化成功");
} catch (IOException e) {
Logger.getGlobal().severe("Server初始化失败" + e.getMessage());
}
}
监听与创建socket
使用CopyOnWriteArrayList来存储socket来管理所有socket.为了保证线程的高效和不至于因为开了太多线程导致服务器崩掉,使用线程池.这几个参数分别是
- 4:核心线程数,也就是保证活跃的数量至少为4个线程(不管有没有任务)
- 8:线程池容量,也就是池中允许活跃的最大线程数
- 10:线程池中除了核心线程之外的空余活跃线程,允许等待新任务的时间
- TimeUnit.SECONDS:时间单位
- new ArrayBlockingQueue<>(Runtime.getRuntime().availableProcessors() * 8):阻塞的线程任务队列,容量为核心数*8
- new MyThreadFactory():这个是自实现的线程工厂,主要用日志记录下线程的创建和抛出的异常,之前照着黄文海的java多线程编程实战指南(核心篇)写的.
其实这里不用线程池也行,把那个监听队列改小.也不怕线程开太多.当然线程池还是性能好些,就是写起来麻烦
通信退出后,得把那个socket从集合中移除
public static void start() {
init();
CopyOnWriteArrayList sockets = new CopyOnWriteArrayList<>();
ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 8, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(Runtime.getRuntime().availableProcessors() * 8), new MyThreadFactory());
while (true) {
executor.execute(() -> {
try (Socket socket = ss.accept()) {
sockets.add(socket);
communicate(socket,sockets);
sockets.remove(socket);
Logger.getGlobal().info("从sockets中移除一个socket");
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
通信
这里没什么说的,注意的是,不要在写完发送的信息就把流关闭了,会把socket也给关闭的.
private static void send(byte[] msg, CopyOnWriteArrayList sockets) throws IOException {
int num = 0;
for (Socket socket : sockets) {
if(socket.isClosed()){
continue;
}
//这里的OutputStream不能关,关闭会释放所有与之相关的资源,也许这个资源包括socket,所以报错了...
OutputStream out = socket.getOutputStream();
out.write(msg);
Logger.getGlobal().info(num++ + "socket接收到了");
}
}
private static void communicate(Socket socket, CopyOnWriteArrayList sockets) {
try (InputStream in = socket.getInputStream()) {
while (socket.isConnected()) {
byte[] msg = new byte[32];
int len = in.read(msg);
String s = new String(msg,0,len);
System.out.println("接收到信息" +s);
if(s.equals("exit") == true){
System.out.println("接收到退出信息,准备退出");
OutputStream out = socket.getOutputStream();
out.write(s.getBytes());
out.flush();
System.out.println("退出");
return;
}
send(Arrays.copyOf(msg, len), sockets);
}
} catch (IOException e) {
Logger.getGlobal().severe("communicate失败" + e.getMessage());
}
}
Client
client把读写分离就好了,就俩线程也用不着线程池,输入exit退出,主线程等读线程退出后再退出(避免抛出异常,毕竟使用的是try with resource,会自动关闭socket,然而此时读线程可能正在读中,未退出,就会抛出异常)
public static void main(String[] args){
try(Socket socket = new Socket(HOST,PORT)){
Logger.getGlobal().info("已经连接到服务器");
Thread thread = new Thread(() -> {
try(InputStream in = socket.getInputStream()){
String words = "true";
do{
byte[] b = new byte[32];
int len = in.read(b);
if(len != -1){
words = new String(b,0,len);
System.out.println(words);
}
}while(words.equals("exit") != true);
}catch (IOException e){
e.printStackTrace();
}
});
thread.start();
OutputStream out = socket.getOutputStream();
Scanner sc = new Scanner(System.in);
System.out.print("请设置昵称:");
String pre = sc.nextLine() + " : ";
while(socket.isConnected()){
String s = sc.nextLine();
if(s.equals("exit") == true){
out.write(s.getBytes());
break;
}
out.write((pre+s).getBytes());
out.flush();
}
thread.join();
System.out.println("等待中");
}catch(UnknownHostException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
完整代码
Server
package cn.informal2019.stu.openjdk8.net.bio.multi;
import cn.informal2019.stu.openjdk8.thread.manage.MyThreadFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.Vector;
import java.util.concurrent.*;
import java.util.logging.Logger;
/**
* @Author zjm
* @Date 2020/3/28
* @Description TODO
* @Version 1.0
*/
public class ServerDemo {
private final static int PORT = 8080;
private static ServerSocket ss;
private static CopyOnWriteArrayList socketTasks = new CopyOnWriteArrayList<>();
private static void init() {
try {
ss = new ServerSocket(PORT);
Logger.getGlobal().info("server 初始化成功");
} catch (IOException e) {
Logger.getGlobal().severe("Server初始化失败" + e.getMessage());
}
}
public static void start() {
init();
CopyOnWriteArrayList sockets = new CopyOnWriteArrayList<>();
ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 8, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(Runtime.getRuntime().availableProcessors() * 8), new MyThreadFactory());
while (true) {
executor.execute(() -> {
try (Socket socket = ss.accept()) {
sockets.add(socket);
communicate(socket,sockets);
sockets.remove(socket);
Logger.getGlobal().info("从sockets中移除一个socket");
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
private static void send(byte[] msg, CopyOnWriteArrayList sockets) throws IOException {
int num = 0;
for (Socket socket : sockets) {
if(socket.isClosed()){
continue;
}
//这里的OutputStream不能关,关闭会释放所有与之相关的资源,也许这个资源包括socket,所以报错了...
OutputStream out = socket.getOutputStream();
out.write(msg);
Logger.getGlobal().info(num++ + "socket接收到了");
}
}
private static void communicate(Socket socket, CopyOnWriteArrayList sockets) {
try (InputStream in = socket.getInputStream()) {
while (socket.isConnected()) {
byte[] msg = new byte[32];
int len = in.read(msg);
String s = new String(msg,0,len);
System.out.println("接收到信息" +s);
if(s.equals("exit") == true){
System.out.println("接收到退出信息,准备退出");
OutputStream out = socket.getOutputStream();
out.write(s.getBytes());
out.flush();
System.out.println("退出");
return;
}
send(Arrays.copyOf(msg, len), sockets);
}
} catch (IOException e) {
Logger.getGlobal().severe("communicate失败" + e.getMessage());
}
}
public static void main(String[] args) {
start();
}
}
Client
package cn.informal2019.stu.openjdk8.net.bio.single;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
import java.util.logging.Logger;
/**
* @Author zjm
* @Date 2020/2/27
* @Description TODO
* @Version 1.0
*/
public class ClientDemo {
public final static int PORT = 8080;
public final static String HOST = "localhost";
public static void main(String[] args){
try(Socket socket = new Socket(HOST,PORT)){
Logger.getGlobal().info("已经连接到服务器");
Thread thread = new Thread(() -> {
try(InputStream in = socket.getInputStream()){
String words = "true";
do{
byte[] b = new byte[32];
int len = in.read(b);
if(len != -1){
words = new String(b,0,len);
System.out.println(words);
}
}while(words.equals("exit") != true);
}catch (IOException e){
e.printStackTrace();
}
});
thread.start();
OutputStream out = socket.getOutputStream();
Scanner sc = new Scanner(System.in);
System.out.print("请设置昵称:");
String pre = sc.nextLine() + " : ";
while(socket.isConnected()){
String s = sc.nextLine();
if(s.equals("exit") == true){
out.write(s.getBytes());
break;
}
out.write((pre+s).getBytes());
out.flush();
}
thread.join();
System.out.println("等待中");
}catch(UnknownHostException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
MyThreadFactory
package cn.informal2019.stu.openjdk8.thread.manage;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
public class MyThreadFactory implements ThreadFactory {
final static Logger LOGGER = Logger.getAnonymousLogger();
private final Thread.UncaughtExceptionHandler ueh;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
public MyThreadFactory(Thread.UncaughtExceptionHandler ueh,String name) {
this.ueh = ueh;
this.namePrefix = name;
}
public MyThreadFactory(){
this(new LoggingUncaughtExceptionHandler(),"thread");
}
protected Thread doMakeThread(final Runnable r){
return new Thread(r){
@Override
public String toString() {
ThreadGroup group =getThreadGroup();
String groupName = null == group ? "" : group.getName();
String threadInfo = getClass().getSimpleName()+"["+getName()+","+getId()+"," + groupName + "]@" + hashCode();
return threadInfo;
}
};
}
@Override
public Thread newThread(Runnable r) {
Thread t = doMakeThread(r);
t.setUncaughtExceptionHandler(ueh);
t.setName(namePrefix + "-" + threadNumber.getAndIncrement());
if(t.isDaemon()){
t.setDaemon(false);
}
if(t.getPriority() != Thread.NORM_PRIORITY){
t.setPriority(Thread.NORM_PRIORITY);
}
if(LOGGER.isLoggable(Level.FINE)){
LOGGER.fine("new thread created" + t);
}
return t;
}
static class LoggingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.log(Level.SEVERE,t + "terminated:",e);
}
}
}
先更后改