多路复用-源码

147 阅读12分钟

架构图

无论是基于socket,还是基于nio channnel,架构是一样的。

最关键的一点就是,服务器端在接受客户端socket连接之后,会为每个客户端socket创建一个专门的socket,以便和客户端socket进行一一通信。


关于端口
1.服务器端
1)服务器套接字 //固定端口,作用是监听客户端连接
2)和客户端通信的套接字 //接收到客户端连接之后,创建专门的socket(随机分配,一般是万级别)与当前客户端进行通信

服务器端有两个socket,作用不同,port也不同。而且2)的socket对每个客户端连接都是一个新的socket。

2.客户端
1)客户端连接服务器的ip和port //服务器ip和port
2)客户端socket自己本身的port //客户端socket自己本身的端口(随机分配,一般是万级别)

客户端只有一个socket,除了自己的port,还有要绑定连接到服务器的ip/port。

最简单的socket程序-服务器只接受一次客户端连接

服务器

package socket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 最简单的socket程序-只接受一次连接
 * <pre>
 * @author gzh
 * @date 2019年7月25日 下午9:06:13
 * </pre>
 */
public class ServerMain {

	public static void main(String[] args) {
		try {
			//创建服务器socket
			ServerSocket serverSocket = new ServerSocket(8080); //一个socket对应一个ip/port,计算机io主要是两块1.磁盘io,即文件io 2.网络io,即socket io。socket io和磁盘文件io的区别是什么?socket io需要提供端口,磁盘文件io不需要port,因为磁盘文件io是在本地,而socket io是网络通信,也就是所谓的网络编程。当前服务器端程序在port 8080上监听。
			
			//接受客户端连接
			Socket socket = serverSocket.accept(); //接受客户端连接成功之后,返回(其实是创建)一个新的socket。这个socket又占了一个新的port,不信你可以打印出来看下。如何打印socket port?有API方法。但是没有设置port啊,port从哪里来的?随机设置了一个,而且你会发现随机设置的一般都是几万,目的是为了避免和其他端口冲突。
			
			//读数据
			InputStream read = socket.getInputStream(); //基于socket,既可以读数据(即可以获取读流),又可以写数据(即可以获取写流)
			int data_read = read.read(); //读一个字节
			System.out.println(data_read);
			
			//写数据
			OutputStream write = socket.getOutputStream();
			int data_write = 2;
			write.write(data_write);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		//程序不关闭,目的是有时间打印读写数据
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
	}

}

客户端


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;

/**
 * 最简单的客户端socket
 * <pre>
 * @author gzh
 * @date 2019年7月25日 下午10:20:47
 * </pre>
 */
public class ClientMain {

	public static void main(String[] args) {
		Socket socket;
		try {
			//创建客户端socket
			socket = new Socket("127.0.0.1", 8080); //Creates a stream socket and connects it to the specified port number on the named host.参数是服务器的ip/port,目的是连接到服务器ip/port。另外,客户端socket的port是随机分配的。
			
			//连接服务器socket
//			InetSocketAddress address = new InetSocketAddress(8080);
//			socket.connect(address); //连接到服务器port8080 //这样写连接不上,必须显式写ip地址
			
			//写数据
			OutputStream write = socket.getOutputStream();
			write.write(1);
			
			//读数据
			InputStream read = socket.getInputStream();
			int data_read = read.read(); //只读一个字节
			System.out.println(data_read);
		} catch (UnknownHostException e1) {
			e1.printStackTrace();
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		
		//程序不关闭
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}


输出结果
1.服务器:1
2.客户端:2

最简单的socket程序-循环接受客户端连接

服务器

package socket2;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 最简单的socket程序-循环接受客户端连接
 * <pre>
 * @author gzh
 * @date 2019年7月25日 下午9:06:13
 * </pre>
 */
public class ServerMain {

	public static void main(String[] args) {
		//创建服务器socket
		ServerSocket serverSocket = null;
		try {
			serverSocket = new ServerSocket(8080); //服务器socket只需要创建一次即可,也就是说,只需要创建一个服务器socket对象,该socket对象始终监听某个固定端口
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} //一个socket对应一个ip/port,计算机io主要是两块1.磁盘io,即文件io 2.网络io,即socket io。socket io和磁盘文件io的区别是什么?socket io需要提供端口,磁盘文件io不需要port,因为磁盘文件io是在本地,而socket io是网络通信,也就是所谓的网络编程。当前服务器端程序在port 8080上监听。

		
		while(true) {
			try {
				
				
				//接受客户端连接
				Socket socket = serverSocket.accept(); //接受客户端连接成功之后,返回(其实是创建)一个新的socket。这个socket又占了一个新的port,不信你可以打印出来看下。如何打印socket port?有API方法。但是没有设置port啊,port从哪里来的?随机设置了一个,而且你会发现随机设置的一般都是几万,目的是为了避免和其他端口冲突。
				
				//读数据
				InputStream read = socket.getInputStream(); //基于socket,既可以读数据(即可以获取读流),又可以写数据(即可以获取写流)
				int data_read = read.read(); //读一个字节
				System.out.println(data_read);
				
				//写数据
				OutputStream write = socket.getOutputStream();
				int data_write = 2;
				write.write(data_write);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		
		
	
	}

}

客户端

package socket2;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;

/**
 * 最简单的客户端socket
 * <pre>
 * @author gzh
 * @date 2019年7月25日 下午10:20:47
 * </pre>
 */
public class ClientMain {

	public static void main(String[] args) {
		Socket socket;
		
		while(true) {
			try {
				//创建客户端socket
				socket = new Socket("127.0.0.1", 8080); //客户端socket的port是随机分配的,而且每次随机分配的port值不一样
				
				//连接服务器socket
//				InetSocketAddress address = new InetSocketAddress(8080);
//				socket.connect(address); //连接到服务器port8080 //这样写连接不上,必须显式写ip地址
				
				//写数据
				OutputStream write = socket.getOutputStream();
				write.write(1);
				
				//读数据
				InputStream read = socket.getInputStream();
				int data_read = read.read(); //只读一个字节
				System.out.println(data_read);
			} catch (UnknownHostException e1) {
				e1.printStackTrace();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}


输出结果
1.服务器:多个1
2.客户端:多个2

最简单的socket程序-多线程

服务器

package socket3;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 最简单的socket程序-循环接受客户端连接,但是这次使用多线程处理每个客户端连接,目的是并发连接可以被多线程同时执行,而不是一个连接一个连接的执行,后面的连接必须等待前面的连接执行完成。
 * 
 * 怎么实现?就是把socket对象丢到线程里去即可。
 * <pre>
 * @author gzh
 * @date 2019年7月25日 下午9:06:13
 * </pre>
 */
public class ServerMain {

	public static void main(String[] args) {
		//创建服务器socket
		ServerSocket serverSocket = null;
		try {
			serverSocket = new ServerSocket(8080); //服务器socket只需要创建一次即可,也就是说,只需要创建一个服务器socket对象,该socket对象始终监听某个固定端口
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} //一个socket对应一个ip/port,计算机io主要是两块1.磁盘io,即文件io 2.网络io,即socket io。socket io和磁盘文件io的区别是什么?socket io需要提供端口,磁盘文件io不需要port,因为磁盘文件io是在本地,而socket io是网络通信,也就是所谓的网络编程。当前服务器端程序在port 8080上监听。

		
		while(true) {
			try {
				
				
				//接受客户端连接
				Socket socket = serverSocket.accept(); //接受客户端连接成功之后,返回(其实是创建)一个新的socket。这个socket又占了一个新的port,不信你可以打印出来看下。如何打印socket port?有API方法。但是没有设置port啊,port从哪里来的?随机设置了一个,而且你会发现随机设置的一般都是几万,目的是为了避免和其他端口冲突。
				
				Thread thread = new Thread(new TaskConnection(socket));
				thread.start();
				
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		
		
	
	}

}


package socket3;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class TaskConnection implements Runnable {
	Socket socket;
	
	TaskConnection(Socket socket){
		this.socket = socket;
	}

	@Override
	public void run() {
		try {
		//读数据
		InputStream read = socket.getInputStream(); //基于socket,既可以读数据(即可以获取读流),又可以写数据(即可以获取写流)
		int data_read = read.read(); //读一个字节
		System.out.println(data_read + ", Thread.currentThread().getName():" + Thread.currentThread().getName());
		
		//写数据
		OutputStream write = socket.getOutputStream();
		int data_write = 2;
		write.write(data_write);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}

/*
 * Runnable 怎么获取线程的名字?
 * 获取线程名字这件事情本质上和Runnable是没有关系的。一个Runnable可以给多个线程去运行,所以如果在这个概念上你有误解的话,希望重新考虑一下。
 * 另外,在任何时候,你都可以用Thread.currentThread().getName()来获取当前线程的名字
 */


客户端

package socket3;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;

/**
 * 最简单的客户端socket
 * <pre>
 * @author gzh
 * @date 2019年7月25日 下午10:20:47
 * </pre>
 */
public class ClientMain {

	public static void main(String[] args) {
		Socket socket;
		
		while(true) {
			try {
				//创建客户端socket
				socket = new Socket("127.0.0.1", 8080); //客户端socket的port是随机分配的,而且每次随机分配的port值不一样
				
				//连接服务器socket
//				InetSocketAddress address = new InetSocketAddress(8080);
//				socket.connect(address); //连接到服务器port8080 //这样写连接不上,必须显式写ip地址
				
				//写数据
				OutputStream write = socket.getOutputStream();
				write.write(1);
				
				//读数据
				InputStream read = socket.getInputStream();
				int data_read = read.read(); //只读一个字节
				System.out.println(data_read);
			} catch (UnknownHostException e1) {
				e1.printStackTrace();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}


输出结果
1.服务器:多个1 //另外,还输出线程名字

1, Thread.currentThread().getName():Thread-0
1, Thread.currentThread().getName():Thread-1
1, Thread.currentThread().getName():Thread-2
1, Thread.currentThread().getName():Thread-3
1, Thread.currentThread().getName():Thread-4
1, Thread.currentThread().getName():Thread-5
1, Thread.currentThread().getName():Thread-6
1, Thread.currentThread().getName():Thread-7
......

2.客户端:多个2

多路复用-没有使用XXXHandler

服务器

package selector;

/**
 * 服务器入口:启动单线程
 * <pre>
 * @author gzh
 * @date 2019年3月20日 上午10:33:27
 * </pre>
 */
public class Server {

	public static void main(String[] args) {
		// 单线程处理任务
		new Thread(new Task1()).start();
		;

	}

}

package selector;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * 单线程处理连接
 * <pre>
 * @author gzh
 * @date 2019年3月20日 上午11:22:42
 * </pre>
 */
public class Task1 implements Runnable {
	private Selector selector = null;

	@Override
	public void run() {
		//创建服务器端套接字通道
		ServerSocketChannel serverSocketChannel = null;
		try {
			serverSocketChannel = ServerSocketChannel.open(); //jdk nio基于通道通信。channel和socket通信的区别是什么?socket同时只能读或写,单向通信;而channel可以同时读和写。底层是硬件-网线就支持双向通信,有的线专门读,有的线专门写。总结,相同点,作用都是通信;不同点是同一时刻一个是只能单向通信一个可以是双向通信,除此之外,没有其他不同的地方。
		    //这里得到(其实是创建)一个channel对象,和socket里的创建socket对象是一模一样的
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		//监听客户端连接的端口
		try {
			serverSocketChannel.socket().bind(new InetSocketAddress(8080),1024); //服务器监听固定port 8080
		    //为什么要根据channel获取socket对象?那channel和socket到底是什么关系?既然最终还是要获取socket对象,为什么要多搞一个channel出来?
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		
		//非阻塞
		try {
			serverSocketChannel.configureBlocking(false);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		//创建多路选择器
		try {
			selector = Selector.open();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		//关联服务器套接字通道和多路选择器
		try {
			serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //监听连接事件
		} catch (ClosedChannelException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		//一直循环轮询
		while (true) {
			//遍历事件
			try {
				selector.select(1000); //阻塞轮询,直到有新的事件到来
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			Set set = selector.selectedKeys(); //处理事件
			Iterator iterator = set.iterator();
			while (iterator.hasNext()) { //遍历处理事件集合
				//取事件
				SelectionKey selectionKey = (SelectionKey) iterator.next();
				
				//处理事件
				handleEvent(selectionKey); //也可以使用XXXHandler处理事件
			}
		}	
	}

	/**
	 * 处理事件
	 * <pre>
	 * 1.连接
	 * 2.读
	 * 3.写
	 * @author gzh
	 * @date 2019年3月20日 上午10:49:23
	 * @param selectionKey
	 * </pre>
	 */
	private void handleEvent(SelectionKey selectionKey) {
		if (selectionKey.isAcceptable()) { //连接事件
			ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
			try {
				SocketChannel socketChannel = serverSocketChannel.accept();
				socketChannel.configureBlocking(false);
				socketChannel.register(selector, SelectionKey.OP_READ); //监听可读事件
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} else if (selectionKey.isReadable()) { //读事件
			SocketChannel socketChannel = (SocketChannel)selectionKey.channel(); //获取当前事件的channel(这里其实和socket的思路是完全一致的,如果是socket,服务器接受客户端连接之后也是创建一个专门的socket和对应客户端通信,这个是一一对应的,即客户端连接(也是socket,有自己的端口)和服务器端的socket,有自己的端口)一一对应,基于这个原理才能保证服务器和客户端一对一的正常通信。
			ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
			try {
				int n = socketChannel.read(byteBuffer); //读客户端写过来的数据
				byte[] bytes = new byte[n];
				byteBuffer.get(bytes);
				String string = new String(bytes, "utf-8");
				System.out.println(string);
				
				//写数据到客户端
				doWrite(socketChannel);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} 
	}

	/**
	 * 写数据到客户端
	 * <pre>
	 * @author gzh
	 * @date 2019年3月20日 上午11:13:15
	 * @param socketChannel
	 * </pre>
	 */
	private void doWrite(SocketChannel socketChannel) {
		String string = "服务器端写数据到客户端";
		
		byte[] bytes = null;
		try {
			bytes = string.getBytes("utf-8");
		} catch (UnsupportedEncodingException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);
		byteBuffer.put(bytes);
		try {
			socketChannel.write(byteBuffer); //写数据到客户端(因为服务器是同一个channel,所以写和刚才的读都是和同一个客户端socket通信)
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

客户端

package selector;

/**
 * 客户端入口:启动客户端单线程
 * <pre>
 * @author gzh
 * @date 2019年3月20日 下午1:35:36
 * </pre>
 */
public class Client {

	public static void main(String[] args) {
		new Thread(new Task2());
		
		
	}

}


package selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * 客户端单线程
 * <pre>
 * @author gzh
 * @date 2019年3月20日 下午1:35:51
 * </pre>
 */
public class Task2 implements Runnable {
	Selector selector = null;

	@Override
	public void run() {
		try {
			//创建客户端套接字通道
			SocketChannel socketChannel = SocketChannel.open();
			//非阻塞模式
			socketChannel.configureBlocking(false);
			//连接服务器
			boolean b = socketChannel.connect(new InetSocketAddress(8080)); //因为异步,所以两种结果:1.立即返回true 2.暂时false
			
			//创建多路选择器
			selector = Selector.open();
			
			
			//1.处理立即成功的情况
			//判断连接是否成功
			if (b) { //连接成功,读事件
				//读事件
				socketChannel.register(selector, SelectionKey.OP_READ);
				//写数据到服务器
				doWrite(socketChannel);
			} else { //不成功,连接事件
				socketChannel.register(selector, SelectionKey.OP_CONNECT);
			}
			
			//2.处理暂时false的情况
			//一直循环轮询
			while (true) {
				//遍历事件
				try {
					selector.select(1000);
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				Set set = selector.selectedKeys();
				Iterator iterator = set.iterator();
				while (iterator.hasNext()) {
					//取事件
					SelectionKey selectionKey = (SelectionKey) iterator.next();
					
					//处理事件
					handleEvent(selectionKey);
				}
			}	
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * 写数据到服务器
	 * <pre>
	 * @author gzh
	 * @param socketChannel 
	 * @date 2019年3月20日 下午1:45:05
	 * </pre>
	 */
	private void doWrite(SocketChannel socketChannel) {
		//写数据到服务器
	}

	/**
	 * 处理事件
	 * <pre>
	 * @author gzh
	 * @date 2019年3月20日 下午1:38:53
	 * @param selectionKey
	 * </pre>
	 */
	private void handleEvent(SelectionKey selectionKey) {
		SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
		
		//连接成功事件
		if (selectionKey.isConnectable()) { 
			try {
				if (socketChannel.finishConnect()) { //连接成功
					//读事件
					socketChannel.register(selector, SelectionKey.OP_READ);
					//写数据到服务器
					doWrite(socketChannel);
				}else { //程序退出
					System.exit(1);
				}
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} 
		
		//读事件
		if (selectionKey.isReadable()) { 
			//读服务器发送的数据
		}
	}

}


说明
java nio-多路复用选择器的demo。


参考
李林锋《netty权威指南》

多路复用-使用XXXHandler