一般情况下的文件读写,虽然之前的BIO较慢,但是大多数场景也基本没啥问题。
但是面对网络IO,动辄数万、十万、百万的并发来说,显然已经可以称之为不可用状态。
所以java很早前就推出了NIO,用来解决这个问题。
本文主要在重提一下阻塞的网络编程,然后在于NIO的实现进行对比。
通俗的说,网络编程就是把数据从一台计算机传输到另一台计算机,这中间需要经过各种中间设备(比如路由器、交换机),也会经过各种软件进行处理加工。
关于计算机网络的发展,可以参考专栏《计算机网络》https://www.crazybytex.com/collection-2.html
网络编程简介
看下百度百科介绍:
众所周知,计算机网络的TCP/IP协议,已经是事实上的标准,所有的操作系统,都提供了底层支持,对于开发人员来说,就是基于操作系统提供的系统调用进行编程,然而TCP/IP协议,较为复杂,比较偏向于底层,所以有了后来的socket,简化了网络编程的复杂度。
SOCKET编程
socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口。
不管是什么协议,从程序的角度看,就是一行行代码的实现,通过一系列代码的组合实现了某种功能,遵守了某些规则的程序。
如同我们平时写代码封装了类库,提供了一系列的接口可以被调用,socket编程的本质就是如此。
这样对于普通的开发来说就不需要过多的涉猎底层网络传输的内容,可以更加高效的专注于应用程序的开发。
从网络编程的视角看,一台计算机的结构大致如下:
现代计算机,肯定是都依赖操作系统的,而操作系统提供了两类接口:
人机交互,比如控制台输入,鼠标点击
编程接口,系统调用函数,供用户开发程序调用系统资源以及管理硬件
socket就是编程接口的一部分,用来网络编程。
tcp网络编程一个概要示意图如下
服务端和客户端初始化 socket,得到文件描述符;
服务端调用 bind,将绑定在 IP 地址和端口;
服务端调用 listen,进行监听;
服务端调用 accept,等待客户端连接;
客户端调用 connect,向服务器端的地址和端口发起连接请求;
服务端 accept 返回用于传输的 socket 的文件描述符;
客户端调用 write 写入数据;服务端调用 read 读取数据;
再详细点说的话,看下UNIX网络编程 接口:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
sockfd是由socket函数返回的套接口描述字,第二个参数是一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度
调用bind 可以指定ip和端口号,也可以不指定
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
sockfd是由socket函数返回的套接口描述字,第二、第三个参数分别是一个指向套接口地址结构的指针和该结构的大小。
套接口地址结构必须含有服务器的IP地址和端口号
如果是TCP套接字,调用connect 将会触发三次握手的过程。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd是bind之后的套接口描述字,第二个参数规定了内核应该为相应套接口排队的最大连接个数
当socket创建时,默认是主动套接字-也就是将会调用connect发起连接的套接字,调用listen那么将会转换为被动套接字,也就是接受指向该连接的套接字
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
//返回:非负描述子——成功,-1——出错
参数sockfd是监听后的套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用一个与这个套接字关联的端口号
参数cliaddr和addrlen是一个结果参数,用来返回已连接客户的协议地址。
如果accept调用成功,那么其返回值是由内核自动生成的一个全新描述符,代表与客户端的连接
socket是一种网络通信模型,与我们常说的同步异步 阻塞非阻塞式没有直接关系的
因为他作为编程接口,底层都有各自的实现,不需要自己单独进行实现
简化以下形式如下
上图就是现在大家进行网络编程的一个通常的模型。
分为客户端和服务端,客户端与服务端建立连接之后,客户端发起请求,服务端返回响应。
对于java开发来说,一个简易的socket编程如下
package com.crazybytex.hello.socket;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @description server
* @Author 本文作者 程序员潇然 疯狂的字节X https://www.crazybytex.com/
* @Date 11:59 2022/7/4
**/
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
Socket socket = null;
Reader reader = null;
OutputStream os = null;
PrintWriter pw = null;
try {
//1、创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
//1024-65535的某个端口
serverSocket = new ServerSocket(9527);
//2、调用accept()方法开始监听,等待客户端的连接
socket = serverSocket.accept();
//3、获取输入流,并读取客户端信息
reader = new InputStreamReader(socket.getInputStream());
char chars[] = new char[1024];
int len;
String strClient = "";
while ((len = reader.read(chars)) != -1) {
strClient = new String(chars, 0, len);
System.out.println("Receive from client message=: " + strClient);
}
//关闭输入流
socket.shutdownInput();
//4、获取输出流,响应客户端的请求
os = socket.getOutputStream();
pw = new PrintWriter(os);
pw.write("Hello client ,response from server");
pw.flush();
//5、关闭资源
pw.close();
os.close();
reader.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.crazybytex.hello.socket;
import java.io.*;
import java.net.Socket;
/**
* @description client
* @Author 本文作者 程序员潇然 疯狂的字节X https://www.crazybytex.com/
* @Date 11:59 2022/7/4
**/
public class SocketClient {
public static void main(String[] args) {
try {
//1、创建客户端Socket,指定服务器地址和端口
Socket socket = new Socket("127.0.0.1", 9527);
//2、获取输出流,向服务器端发送信息
//字节输出流
OutputStream os = socket.getOutputStream();
//将输出流包装成打印流
PrintWriter pw = new PrintWriter(os);
pw.write("hello server ,i'm client");
pw.flush();
socket.shutdownOutput();
//3、获取输入流,并读取服务器端的响应信息
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String info = null;
while ((info = br.readLine()) != null) {
System.out.println("Receive from server message=: " + info);
}
//4、关闭资源
br.close();
is.close();
pw.close();
os.close();
socket.close();
} catch (IOException e) {
System.out.println(e);
}
}
}
值得注意的是,如果在如图所示位置加断点
server启动后,并不会走到断点,当有客户端连接发送数据时,才会走到断点
这也说明了 accept方法是一个阻塞方法。
对于java中的socket编程,核心概念如上图所示,基于请求响应模式
服务端监听端口,等待远程连接
客户端根据远程服务器的ip地址以及端口号,发起连接
建立连接后,基于流,进行请求和响应。
socket是应用层与传输层之间的编程接口,socket就是对底层函数的封装,java是面向对象的编程语言
所以java中的socket就是对这些方面的一个抽象
一种通俗的网络编程模式如下图所示,通道不仅限于InputStream 和OutputStream ,随着java的发展,随之而来的还有Channel
换句话说,大致的网络编程模型是基本固定的,而变化的是IO的具体处理
介绍了阻塞式的网络编程,下面展示一个非阻塞的。
示例代码gitee:
https://gitee.com/crazybytex/java-code-fragment/tree/master/src/main/java/com/crazybytex/fragment/nio
package com.crazybytex.fragment.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/**
* @Author 本文作者 程序员潇然 疯狂的字节X https://www.crazybytex.com/
* @Date 2022/10/10 14:41
* @Description TODO
**/
public class Server {
public static void main(String[] args) throws IOException, InterruptedException {
//1.打开一个服务器通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2.绑定对应的端口号
serverSocketChannel.bind(new InetSocketAddress(8888));
//3.通道默认是阻塞的,需要设置为非阻塞
serverSocketChannel.configureBlocking(false);
//4.创建选择器
Selector selector = Selector.open();
//5.将服务器通道注册到选择器上,并且指定注册监听事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端启动成功!");
while (true) {
//6.检查选择器是否有事件
int select = selector.select(2000);
if (select == 0) {
continue;
}
//7.获取事件集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//8.判断事件是否是客户端连接事件SelectionKey.isAcceptable()
SelectionKey key = iterator.next();
//9.得到客户端通道,并将通道注册到选择器上,并且指定监听事件为OP_READ
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端已连接......" + socketChannel);
//必须设置通道为非阻塞, 因为selector需要轮询监听每个通道的事件
socketChannel.configureBlocking(false);
//并指定监听事件为OP_READ
socketChannel.register(selector, SelectionKey.OP_READ);
}
//10.判断是否是客户端读就绪事件SelectionKey.isReadable()
if (key.isReadable()) {
//11.得到客户端通道,读取数据到缓冲区
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffer);
if (read > 0) {
System.out.println("客户端消息:" +
new String(byteBuffer.array(), 0, read,
StandardCharsets.UTF_8));
//12.给客户端回写数据
socketChannel.write(ByteBuffer.wrap("收到消息了".getBytes(StandardCharsets.UTF_8)));
socketChannel.close();
}
}
//13.从集合中删除对应的事件, 因为防止二次处理.
iterator.remove();
}
}
}
}
package com.crazybytex.fragment.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
/**
* @Author 本文作者 程序员潇然 疯狂的字节X https://www.crazybytex.com/
* @Date 2022/10/10 14:41
* @Description TODO
**/
public class Client {
public static void main(String[] args) throws IOException {
//1.打开通道
SocketChannel socketChannel = SocketChannel.open();
//2.设置连接IP和端口号
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
//3.写数据
socketChannel.write(ByteBuffer.wrap("服务端你好".getBytes(StandardCharsets.UTF_8)));
//4.读取服务器写回的数据
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(readBuffer);
System.out.println("服务端返回消息:" + new String(readBuffer.array(), 0, read, StandardCharsets.UTF_8));
//5.释放资源
socketChannel.close();
}
}
先运行server
在启动客户端
再看下服务端
从执行结果看,也没有多大区别,但是实现方式完全变化了。
ServerSocketChannel 绑定了本地的端口,并且注册到了Selector上
通过Selector可以获取事件集合,遍历这个集合可以获得就绪的事件
根据事件的类型,可以进行相关的处理逻辑。
不再是直接的通过socket建立连接后的单纯阻塞读取,演变成了基于事件驱动的模型。
抽象出来了Selector SelectionKey Channel 等概念,Channel 关联到Selector,注册绑定关注的SelectionKey(事件),当事件产生响应时,就会获得相应的回调(也就是进入了相关事件的处理逻辑),这样不需要关注过程,只需要写相关的处理逻辑即可。
这是过程就是事件注册与处理结果回调,这个与前面提到的IO多路复用是不是有点类似呢?
毕竟上层应用,不需要关注底层,只需要关注事件的注册以及相关事件的处理即可。
后续展开介绍网络编程模型。
转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-202-1-1.html