JAVA网络编程有三种方式,IO也就是BIO,BIO的伪异步方式,和NIO,原理都是通过socket(套接字进行通信)
套接字:就是ip+port ip就是计算机的地址 在java中默认是本地ip 127.0.0.1,port是端口号,每一个应用程序都有自己的端口号。每一台电脑的ip都不一样,每一台电脑上不能同时存在两个端口相同的程序,这样就可以确保网络编程通信的准确性,在其他编程语言中也是一样。
首先我们先通过看几个deme 来实现网络编程,再来比较几种方式的不同。
IO:
server服务端,首先创建一个serverSocket 其实也就是socket,java中在服务端喜欢用serversocket来表示服务端的socket,ip是默认的端口号是9876,io方式中采用阻塞的方式,当客户端有连接进来后才会结束阻塞返回一个socket也就是客户端的socket,并启动一个线程来监听这个socket,接着会继续阻塞等待下一个客户端socket的到来
public class IOServer {
//端口
private static final int port = 9876;
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
//阻塞
Socket socket = serverSocket.accept();
//创建一个新的线程来监听
new Thread(new IOServerHandler(socket) ).start();;
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
在来看一下IOServerHandler的代码,因为这个类是通过一个线程来监听的 所以一定要实现runnable接口,io是读写分离的,inputStream是读的流,outputstream是写的流,读到的和写出的都是通过byte数组的来读写的,数组的长度决定一次读的大小,当读到的返回值是-1时,表示没有数据可读,切记inputStream的read方法是阻塞的当读完数据的时候如果没有新的数据可读,程序不会往下走会阻塞到这里 直到读到新的数据,使用完之后记得关闭所有的流
public class IOServerHandler implements Runnable{
private Socket socket;
public IOServerHandler(Socket socket){
this.socket = socket;
}
public void run() {
InputStream in = null;
OutputStream out = null;
try {
in = socket.getInputStream();
out = socket.getOutputStream();
//读取客户端的 消息
byte[] temp = new byte[1024];
String pri = "";
int length = 0;
while(true){
length = in.read(temp);
if(length == -1){
break;
}
pri = new String(temp,0,length);
System.out.println(pri);
String wri = "服务器收到信息。。";
out.write(wri.getBytes());
}
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
out.flush();
} catch (IOException e1) {
e1.printStackTrace();
}
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
再来看客户端代码:客户端想要和服务端进行通信 一定要知道服务端的ip和端口号22,并创建连接,当new了一个socket之后,这个socket就会进入服务端的accept处,将这个socket返回到服务端,建立连接。流的方式和服务端得使用是一样的 读写分离
public class Client {
private static final int port = 9876;
private static final String host = "127.0.0.1";
public static void main(String[] args) {
Socket socket = null;
InputStream in = null;
OutputStream out = null;
try {
socket = new Socket(host,port);
in = socket.getInputStream();
out = socket.getOutputStream();
out.write(new String("连接到服务器。。。").getBytes());
byte[] temp = new byte[1024];
int length = 0;
String pri = "";
while(true){
length = in.read(temp);
if(length == -1){
break;
}
pri = new String(temp,0,length);
System.out.println(pri);
}
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
看完服务端和客户端的 代码之后,大家想必明白当有一个客户端进行连接的时候会创建一个新的线程,除非旧的连接断开线程才会结束,这样来说如果你的电脑最多只能承受1000个线程,那么你的服务器最多只能承载1000个用户。
伪异步的io网络编程是通过线程池的方式来管理线程,并可设置队列等待的线程数,相对的可以增加服务器的承载力,我们来看一下服务端代码:
和上面一样是创建一个serversocket 也是阻塞之后返回一个socket连接,不同的是这里并没有直接new一个线程,而是通过线程池来管理连接
public class PoolIOServer {
private static final int port = 9876;
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
Socket socket = null;
ServerHandlerExecutePool executePool = new ServerHandlerExecutePool(50,100);
while(true){
socket = serverSocket.accept();
executePool.execute(new IOServerHandler(socket));
}
} catch (IOException e) {
e.printStackTrace();
} finally{
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
看一下线程池的代码:使用工厂模式来创建线程,方便对线程的管理
Runtime.getRuntime().availableProcessors() 是coreThreadSize表示的是核心的线程数,也就是我们cup的最大线程数,比如说电脑配置的4核8线程,maxPoolSize是我们设置得到最大线程数,120L是表示120秒的等待时间,当一个线程120秒没有活跃会自动断开给其他客户端腾出位置,secnds是秒的意思就是120的单位,queuesieze队列的大小,排队的大小
public class ServerHandlerExecutePool {
private ExecutorService executor;
public ServerHandlerExecutePool(int maxPoolSize,int queueSize){
//Runtime.getRuntime().availableProcessors()本地方法 cpu最大的线程数
//maxPoolSize 最大线程数
//120L保持活跃的时间
//TimeUnit.SECONDS 单位
//queueSize 队列的容量
this.executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize),new ExecuteThreadFactory("executePool"));
}
public void execute(Runnable task){
executor.execute(task);
}
}
可以显而易见相比较与第一种方式用线程池的方式管理 可以略微的提高性能
在看一下nio 的方式;首先我们 要了解nio的三个基础概念:
buffer(缓冲区)中定义了大量的put get flip 方法,position mark等参数,大大方便了对读写内容的操作
channel(通道)不存在inputstream和outputStream,直接通过channel就可以实现两端的通信
selector是选择器,每个channel会都会注册的奥selector中得到一个key会不断轮询找到可以进行通信的channel进行调用
通过channel提升了传输效率,通过selector可以大大利用起闲置的线程,能承载1000线程的电脑不在是支持1000个玩家,而是能支持1000个线程去并发。看一下服务器代码:
public class NioServer implements Runnable{
//buffer:缓冲区 本质是一个数组
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
//多路复用器
private Selector selector;
public NioServer(int port){
try{
//1.打开多路复用器
this.selector = Selector.open();
//2.打开服务器端的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3.设置通道的阻塞模式
ssc.configureBlocking(false);
//4.绑定地址
ssc.bind(new InetSocketAddress(port));
//5.把服务器通道注册到多路复用器 监听的状态
ssc.register(this.selector,SelectionKey.OP_ACCEPT);
}catch(IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new NioServer(9876)).start();;
}
public void run() {
while(true){
try {
//1.必须让多路复用器 开始监听
this.selector.select();
//2.返回多路复用器所有注册的key selectedKeys()返回值是set
Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();
//3.遍历获取的key
while(it.hasNext()){
//4.接收key的值
SelectionKey key = it.next();
//5.从容器中移除已经被选中的key 缓解压力删掉没有意义的key
it.remove();
//6.验证操作 判断key是否有效 true是有效
if(key.isValid()){
//7.如果 为阻塞状态
if(key.isAcceptable()){
this.accept(key);
}
//8.如果是可读状态
if(key.isReadable()){
this.read(key);
}
//9.如果是可写状态
if(key.isWritable()){
this.write(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void write(SelectionKey key) {
}
private void read(SelectionKey key) {
try{
//1.对缓冲区进行清空
this.readBuffer.clear();
//2.获取之前注册的socketchannel通道对象
SocketChannel sc = (SocketChannel)key.channel();
//3.从通道里获取数据放入缓冲区
int index = sc.read(this.readBuffer);
if(index == -1){
key.channel().close();
key.cancel();
return;
}
}catch(IOException e){
e.printStackTrace();
}
//读取readbuffer数据 然后打印到控制台
//4.由于sc通道里的数据到readbuffer容器中 ,所以readbuffer里面的position一定发生了变化 要进行复位
this.readBuffer.flip();
byte[] bytes = new byte[this.readBuffer.remaining()];
this.readBuffer.get(bytes);
String body = new String(bytes).trim();
System.out.println("服务器读取数据:"+body);
}
/**
* 监听阻塞状态方法执行
* @param key
*/
private void accept(SelectionKey key) {
try {
//1.由于目前是server端 ,那么一定是server端启动并且处于阻塞状态 所哟煀阻塞状态的key 一定是:serversocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
//2.通过调用accept方法 返回一个具体的客户端连接句柄
SocketChannel sc = serverSocketChannel.accept();
//3.设置客户端通道为非阻塞
sc.configureBlocking(false);
//4.设置当前获取的客户端连接状态为可读状态位
sc.register(this.selector,SelectionKey.OP_READ);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
通过上面的内容我们做出如下总结:
1.io是面向流 nio是面向缓冲
2.io是阻塞 nio是非阻塞
3.io 一个线程管理一个连接 nio一个线程管理多个连接(一个线程管理一个selector,一个selector管理所有通道),使nio可容纳的客户端更多
4.io是读写分离的 nio读写可以使用一个buffer 一个channel使nio传输速度更快
5.nio唯一的劣势,难度相比于io要大很多,但是过netty的架构来简化代码