通信主要是两个要点:消息处理和消息传输
- 消息处理:读取数据或者写入数据(同步信息采用BIO或NIO,异步信息采用AIO)
- 消息传输:借助网络协议(TCP/IP和UDP/IP)来实现消息传输
四种方法实现基于消息方式实现系统间通信:
- TCP/IP+BIO
- TCP/IP+NIO
- UDP/IP+BIO
- UDP/IP+NIO
目录
一、 术语解释
- TCP/IP:
一种可靠的网络数据传输协议。要求通信双方先建立连接,再进行通信。(保证数据传输的可靠性,会牺牲一些性能) - UDP/IP:
一种不可靠的网络数据传输协议。并不直接给通信双方建立连接,而是发送到网络上通信。(不能保证数据传输的可靠,性能较好) - BIO:
同步阻塞IO,数据的读取写入必须阻塞在一个线程内等待其完成。(一旦有请求链接,则新建一个线程,来处理这次请求,效率很低,无法实行高并发场景) - NIO:
同步非阻塞IO,通过通道和缓冲区来实现非阻塞,当有流可读或者可以写时,操作系统会通知应用程序进行处理,应用再将流读取到缓冲区或写入操作系统。(不再是一个连接就要对应一个处理线程) - AIO:
异步非阻塞I/O,应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
二、 TCP/IP+BIO
基于Socket、ServerSocket来实现TCP/IP+BIO的系统通信。
- Socket:用于实现建立连接
- ServerSocket:用于实现服务器端口的监听
为了满足服务端可以同时接受多个请求,最简单的方法是生成多个Socket,会产生两个问题:
- 生成太多Socket会消耗过多资源
- 频繁地创建Socket还会影响性能。
为了解决上面的问题,通常采用连接池维护Socket。
1. 服务端
- 服务端使用ServerSocket监听端口
- 等待客户端连接,只有连接上时才运行后面的操作,否则一直堵塞
- 连接成功后,通知客户端连接成功
- 读取客户端发送的信息,没有发送则一直堵塞,读取到信息时通知客户端:服务端已收到信息
try {
//服务端监听端口8086
ServerSocket serverSocket = new ServerSocket(8086);
while (true){
//同步阻塞,直到建立连接
Socket client = serverSocket.accept();
//客户端连接成功时,发送信息给客户端
OutputStream outputStream = client.getOutputStream();
outputStream.write(("你【"+client.getPort()+"】"+"成功连接到服务端端口8086").getBytes());
outputStream.flush();
//服务端控制台打印连接成功信息
System.out.println("客户端【" + client.getPort()+"】已连接");
try {
//获取客户端输入的信息
InputStream inputStream = client.getInputStream();
byte[] bytes = new byte[1024];
while (inputStream.read(bytes)!=-1){
//收到信息时,通知客户端,服务端已收到信息
outputStream.write(("服务端已收到你的信息"+new String(bytes)).getBytes());
outputStream.flush();
//将接收到的信息打印到控制台
System.out.println("收到信息:"+new String(bytes));
}
}catch (IOException e){
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
2. 客户端
- 客户端使用Socket连接服务端
- 启动线程实时监听服务端发送来的信息
- 在控制台输入信息,发送到服务端
- 当输入exit命令时,关闭连接
try {
//控制台输入信息(用于发送信息给服务端)
Scanner scanner = new Scanner(System.in);
//创建连接
Socket socket = new Socket("127.0.0.1",8086);
//启动线程监听服务端发来的信息
new Thread(()->{
//读取服务端发来的信息
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
byte[] bytes = new byte[1024 * 4];
while ((inputStream.read(bytes)) != -1) {
System.out.println("收到服务端消息:" + new String(bytes));
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
OutputStream outputStream = socket.getOutputStream();
while(true){
//读取控制台输入的内容
String content = scanner.nextLine();
//输入exit,结束通讯
if(content.equals("exit")){
//关闭输出流
outputStream.close();
//断开连接
socket.close();
}else{
outputStream.write(content.getBytes());
outputStream.flush();
}
}
} catch (IOException e) {
e.printStackTrace();
}
3. 测试
启动上述的服务端、客户端,控制台提示连接成功。
服务端:
客户端
客户端发送信息到服务端。
客户端:
服务端:
输入exit命令,客户端关闭连接
客户端:
三、TCP/IP+NIO
基于Clannel(SocketClannel和ServerSocketChannel)和Selector的相关类来实现TCP/IP+NIO方式的系统间通信
- SocketClannel: 用于建立连接、监听事件及操作读写。
- ServerSocketClannel: 用于监听端口即监听连接事件。
- Selecter: 获取是否有要处理的事件。
3.1 服务端
- 开启通道用于监听端口
- 创建选择器,将通道注册到选择器中
- 等待客户连接,接入前阻塞
- 选择器论查询,检测就绪情况
- 获取就绪集合
- 根据就绪事件类型,调用对应得业务处理方法
public class Main {
public void start() throws IOException {
//创建选择器
Selector selector = Selector.open();
//创建Channel通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//为channel通道绑定监听端口
serverSocketChannel.bind(new InetSocketAddress(8086));
//设置channel为非阻塞模式
serverSocketChannel.configureBlocking(false);
//将通道注册到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//监听连接事件
System.out.println("服务器启动成功!");
//循环等待接入
while(true){//while(true) c for;;
//获取可用channel数量
int readyChannels = selector.select();
if (readyChannels == 0) continue;
//获取可用channel的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
// selectionKey实例
SelectionKey selectionKey = (SelectionKey) iterator.next();
//移除Set中的当前selectionKey
iterator.remove();
/**
* 根据就绪事件类型,调用对应业务处理方法
*/
//密钥的通道已准备好可接入
if (selectionKey.isAcceptable()) {
acceptHandler(serverSocketChannel, selector);
}
// 密钥的通道已准备好可读取
if (selectionKey.isReadable()) {
readHandler(selectionKey,selector);
}
}
}
}
/**
* 接入事件处理器
*/
private void acceptHandler(ServerSocketChannel serverSocketChannel,
Selector selector) throws IOException {
//创建socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//设置为非阻塞工作模式
socketChannel.configureBlocking(false);
//将channel注册到selector上,监听可读事件
socketChannel.register(selector, SelectionKey.OP_READ);
//回复客户端提示信息
socketChannel.write(ByteBuffer.wrap("已成功连接服务端:8086".getBytes()));
}
//可读事件处理器
private void readHandler(SelectionKey selectionKey,
Selector selector) throws IOException{
//创建socketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//设置为非阻塞工作模式
socketChannel.configureBlocking(false);
//创建按buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//读取客户端请求内容
StringBuffer requestContent = new StringBuffer();
if(socketChannel.read(byteBuffer)>0){
//切换buffer为读模式
byteBuffer.flip();
requestContent.append(Charset.forName("UTF-8").decode(byteBuffer));
}
System.out.println("接收到客户端信息:"+requestContent);
}
public static void main(String[] args) throws IOException {
Main main = new Main();
main.start();
}
}
3.2 客户端
ClientHandler端
/**
* 用于接收服务端的消息
*/
public class NioClientHandler implements Runnable{
private Selector selector;
public NioClientHandler(Selector selector){
this.selector = selector;
}
@Override
public void run() {
try{
while(true){
//获取选择器中的可用通道数
int readChannel = selector.select();
if(readChannel==0){
return;
}
Set<SelectionKey> selectionKeys =selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
//事件为可读事件时
if(selectionKey.isReadable()){
readHandler(selectionKey,selector);
}
}
}
}catch (IOException e){
e.printStackTrace();
}
}
//可读事件处理器
private void readHandler(SelectionKey selectionKey,
Selector selector) throws IOException{
//创建socketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//设置为非阻塞工作模式
socketChannel.configureBlocking(false);
//创建按buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//读取客户端请求内容
StringBuffer requestContent = new StringBuffer();
if(socketChannel.read(byteBuffer)>0){
//切换buffer为读模式
byteBuffer.flip();
requestContent.append(Charset.forName("UTF-8").decode(byteBuffer));
}
//打印服务端发送来的信息
System.out.println(requestContent);
}
}
Client端
public class Main {
public static void main(String[] args) {
//控制台输入信息(用于发送信息给服务端)
Scanner scanner = new Scanner(System.in);
try {
//打开一个通道
SocketChannel channel = SocketChannel.open();
//创建选择器
Selector selector= Selector.open();
//设为非阻塞模式
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
new Thread(new NioClientHandler(selector)).start();
//想服务端发起连接请求
if(!channel.connect(new InetSocketAddress("127.0.0.1",8086))){
//不断轮查连接状态,直到连接成功
while (!channel.finishConnect()){
//非阻塞在等待连接成功的期间,可以做其他操作
System.out.println("还没连上,只能摸鱼....");
}
}
System.out.println("连接成功,结束非阻塞");
//将控制台输入的信息发送到服务端
while(scanner.hasNextLine()){
channel.write(ByteBuffer.wrap(scanner.nextLine().getBytes()));
}
//关闭信道
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、UDP/IP+BIO
Java对UDP/IP方式的网络数据传输同样采用Socket机制,只是UDP/IP下的Socket没有建立连接,因此无法双向通信。如果需要双向通信,必须两端都生成UDP Server。
Java中通过DatagramSocket和DatagramPacket来实现UDP/IP+BIO方式和系统间通信。
- DatagramSocket:负责监听端口和读写数据
- DatagramPacket:作为数据流对象进行传输
由于UDP双端不建立连接,所以也就不存在竞争问题,只是最终读写流的动作是同步的。
4.1 代码
客户端和服务端都类似
//控制台输入信息(用于发送信息给目标)
Scanner scanner = new Scanner(System.in);
//监听端口8086
DatagramSocket socket = new DatagramSocket(8086);
new Thread(()->{
try {
//创建接收数据用的对象
DatagramPacket receivePacket = new DatagramPacket(new byte[1024],1024);
//接收信息
socket.receive(receivePacket);
System.out.println("接收的信息是:"+new String(receivePacket.getData(),0,receivePacket.getLength()));
} catch (IOException e) {
e.printStackTrace();
}
}).start();
while(true){
String content = scanner.nextLine();
//创建发送数据用的对象,发送到端口为8087的服务端
DatagramPacket sendPacket = new DatagramPacket(content.getBytes(),content.length(), InetAddress.getLocalHost(),8087);
socket.send(sendPacket);
}
4.2 示例
启动两个UDP Server,分别监听端口:8086、8087。8086向8087发送信息,8087控制台打出接收到的信息。
五、UDP/IP+NIO
通过DatagramClannel和ByteBuffer来实现UDP/IP方式的系统间通信。
- DatagramClannel:负责监听端口及进行读写
- ByteBuffer:用于数据传输
客户端和服务端双方通信的方法都类似
5.1 发送信息
public void send(){
Scanner scanner= new Scanner(System.in);
//创建监听通道
DatagramChannel sendChannel=null;
//创建选择器
Selector selector = null;
try {
//开启监听通道
sendChannel = DatagramChannel.open();
//设置非阻塞
sendChannel.configureBlocking(false);
//设置监听地址
SocketAddress target = new InetSocketAddress("127.0.0.1",8086);
//通道监听地址
sendChannel.connect(target);
//开启选择器
selector = Selector.open();
//注册选择器
sendChannel.register(selector, SelectionKey.OP_WRITE);
while(true){
if(selector.select()==0){
continue;
}
Iterator<SelectionKey> keyIter=selector.selectedKeys().iterator();
while(keyIter.hasNext()){
SelectionKey key = keyIter.next();
keyIter.remove();
//事件为可写事件时
if(key.isWritable()){
ByteBuffer buffer= ByteBuffer.wrap(scanner.nextLine().getBytes());
DatagramChannel channel = (DatagramChannel) key.channel();
//像通道写入数据
channel.write(buffer);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭通道
try {
sendChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.2 接收信息
public static void receive(){
//创建选择监听通道
DatagramChannel receiveChannel = null;
//创建选择器
Selector selector = null;
try {
//开启监听通道
receiveChannel = DatagramChannel.open();
//设置非阻塞
receiveChannel.configureBlocking(false);
//开启监听端口8086
DatagramSocket socket=receiveChannel.socket();
socket.bind(new InetSocketAddress(8086));
//开启选择器
selector=Selector.open();
//注册选择器
receiveChannel.register(selector, SelectionKey.OP_READ);
while(true){
if(selector.select()==0){
continue;
}
Iterator<SelectionKey> keyIter=selector.selectedKeys().iterator();
while(keyIter.hasNext()){
SelectionKey key = keyIter.next();
if(key.isReadable()){
DatagramChannel channel = (DatagramChannel) key.channel();
ByteBuffer buffer= ByteBuffer.allocate(2000);
channel.receive(buffer);//注意UDP/IP+NIO这个是receive
String msg=new String(buffer.array());
System.out.println("收到信息:"+msg);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭通道
receiveChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}