Java网络编程
网络编程的基础知识
Socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
主机 A 的应用程序要能和主机 B 的应用程序通信,必须通过 Socket 建立连接,而建立 Socket 连接必须需要底层TCP/IP 协议来建立 TCP 连接。建立 TCP 连接需要底层 IP 协议来寻址网络中的主机。
我们知道网络层使用的 IP 协议可以帮助我们根据 IP 地址来找到目标主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信就要通过 TCP 或 UPD 的地址也就是端口号来指定。这样就可以通过一个 Socket 实例唯一代表一个主机上的一个应用程序的通信链路了。
短连接与长连接
短连接:
连接->传输数据->关闭连接
传统HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。
也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。
长连接:
连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。
长连接指建立SOCKET连接后不管是否使用都保持连接。
什么时候用长连接,短连接?
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源。
网络通讯流程
在通信编程里提供服务的叫服务端,连接服务端使用服务的叫客户端。
在开发过程中,如果类的名字有Server或者ServerSocket的,表示这个类是给服务端容纳网络服务用的,如果类的名字只有Socket的,那么表示这是负责具体的网络读写的。那么对于服务端来说ServerSocket就只是个场所(娱乐场所),具体和客户端沟通的还是一个一个的socket(娱乐事件),所以在通信编程里,ServerSocket并不负责具体的网络读写,ServerSocket就只是负责接收客户端连接后,新启一个socket来和客户端进行沟通。这一点对所有模式的通信编程都是适用的。
在通信编程里,我们关注的其实也就是三个事情
1、连接(客户端连接服务器,服务器等待和接收连接)
2、读网络数据
3、写网络数据
所有模式的通信编程都是围绕着这三件事情进行的。服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。
JDK网络编程(BIO)
传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。代码如下:
public class BIOClient {
public static void main(String[] args) {
Socket client = new Socket();
try {
client.connect(new InetSocketAddress("127.0.0.1", 12345));
System.out.println("客户端发送数据:");
BufferedReader sys=new BufferedReader(new InputStreamReader(System.in));
String msg = sys.readLine();
BufferedWriter bo = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
bo.write(msg);
bo.newLine();
bo.flush();
msg = br.readLine();
System.out.println("接受服务器消息:" + msg);
bo.close();
br.close();
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class BIOServer {
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(12345);
System.out.println("服务端启动");
while (true) {
new Thread(new ServerTask(ss.accept())).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ServerTask implements Runnable {
Socket socket;
public ServerTask(Socket s) {
this.socket = s;
}
@Override
public void run() {
try {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = br.readLine();
System.out.println("接收客户端数据:" + msg);
bw.write("hello client");
bw.newLine();
bw.flush();
bw.close();
br.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (Exception e) {
}
}
}
}
NIO网络编程
网络三种I/O模型
分类:
BIO:(同步 阻塞)jdk1.4以前 java.io包
NIO:(同步 非阻塞)jdk1.4 java.nio包
AIO:(异步 非阻塞)jdk1.7 java.nio包
如何如何理解:
当我们去吃饭可以有以下几种模式:
饭店 -> 服务器
饭菜-> 数据
饭菜好了-> 数据就绪
端菜 /送菜 -> 数据读取
BIO:食堂排队打饭模式:排队在窗口,打好才走;
NIO:点单、等待被叫模式:等待被叫,好了自己去端;
AIO:包厢模式:点单后菜直接被端上桌。
阻塞与非阻塞
菜没好,要不要死等 -> 数据就绪前要不要等待?
阻塞: 没有数据传过来时,读会阻塞直到有数据;缓冲区满时,写操作也会阻塞。
非阻塞: 遇到这些情况,都是直接返回
同步与异步
菜好了,谁端 -> 数据就绪后,数据操作谁完成?
同步: 数据就绪后需要自己去读是同步
异步: 数据就绪直接读好再回调给程序是异步
NIO和BIO的主要区别
面向流与面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
NIO三大核心组件
NIO有三大核心组件:Selector选择器、Channel管道、buffer缓冲区。
Selector
Selector的英文含义是“选择器”,也可以称为为“轮询代理器”、“事件订阅器”、“channel容器管理机”都行。
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器(Selectors),然后使用一个单独的线程来操作这个选择器,进而“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
应用程序将向Selector对象注册需要它关注的Channel,以及具体的某一个Channel会对哪些IO事件感兴趣。Selector中也会维护一个“已经注册的Channel”的容器。
Channel
通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据,而且可以同时进行读写。
- 所有被Selector(选择器)注册的通道,只能是继承了SelectableChannel类的子类。
- ServerSocketChannel:应用服务器程序的监听通道。只有通过这个通道,应用程序才能向操作系统注册支持“多路复用IO”的端口监听。同时支持UDP协议和TCP协议。
- ScoketChannel:TCP
Socket套接字的监听通道,一个Socket套接字对应了一个客户端IP:端口到服务器IP:端口的通信连接。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
buffer缓冲区
网络通讯中负责数据读写的区域
NIO工作流程图
NIO网络编程
客户端
public class Client {
private static NioClientHandle nioClientHandle;
public static void start() {
if (null == nioClientHandle) {
nioClientHandle = new NioClientHandle("127.0.0.1", 5555);
}
new Thread(nioClientHandle, "client").start();
}
public static boolean sendMsg(String msg) throws IOException {
nioClientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args) throws IOException {
start();
Scanner sc = new Scanner(System.in);
while (Client.sendMsg(sc.next())) ;
}
public class NioClientHandle implements Runnable {
private String ip;
private Integer port;
private Selector selector;
private SocketChannel channel;
private volatile boolean started;
public NioClientHandle(String ip, int port) {
this.ip = ip;
this.port = port;
try {
//创建选择器
selector = Selector.open();
//打开监听通道
channel = SocketChannel.open();
//false :非阻塞 true: 阻塞
channel.configureBlocking(false);
started = true;
} catch (IOException e) {
e.printStackTrace();
}
}
public void stop() {
this.started = false;
}
@Override
public void run() {
boolean connect = false;
try {
connect = channel.connect(new InetSocketAddress(ip, port));
if (connect) {
channel.register(selector, SelectionKey.OP_READ);
} else {
channel.register(selector, SelectionKey.OP_CONNECT);
}
} catch (IOException e) {
e.printStackTrace();
}
while (started) {
try {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
handle(key);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handle(SelectionKey key) throws IOException {
if (key.isValid()) {
SocketChannel channel = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (channel.finishConnect()) {
channel.register(selector, SelectionKey.OP_READ);
} else {
System.exit(-1);
}
}
if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(buffer);
if (readBytes > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println(new String(bytes, StandardCharsets.UTF_8));
}
}
}
}
public void sendMsg(String msg) throws IOException {
doWrite(channel, msg);
}
public static void doWrite(SocketChannel channel, String msg) throws IOException {
//消息转成字节数据
byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
//开辟一个内存空间
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
//消息写入ByteBuffer
writeBuffer.put(bytes);
//翻转,充值ByteBuffer
writeBuffer.flip();
//通过channel写出数据
channel.write(writeBuffer);
}
}
服务端
public class NioServer {
private static NioServerHandle nioServerHandle;
public static void start(){
if(nioServerHandle !=null) nioServerHandle.stop();
nioServerHandle = new NioServerHandle(5555);
new Thread(nioServerHandle,"Server").start();
}
public static void main(String[] args){
start();
}
}
public class NioServerHandle implements Runnable{
private Selector selector;//reactor
private ServerSocketChannel serverChannel;
private volatile boolean started;
public NioServerHandle(int port) {
try{
//创建选择器
selector = Selector.open();
//打开监听通道
serverChannel = ServerSocketChannel.open();
//如果为 true,则此通道将被置于阻塞模式;
// 如果为 false,则此通道将被置于非阻塞模式
serverChannel.configureBlocking(false);//开启非阻塞模式
serverChannel.socket().bind(new InetSocketAddress(port));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
//标记服务器已开启
started = true;
System.out.println("服务器已启动,端口号:" + port);
}catch(Exception e){
e.printStackTrace();
System.exit(1);
}
}
public void stop(){
started = false;
}
@Override
public void run() {
//循环遍历selector
while(started){
try{
//阻塞,只有当至少一个注册的事件发生的时候才会继续.
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
}catch(Throwable t){
t.printStackTrace();
}
}
//selector关闭后会自动释放里面管理的资源
if(selector != null)
try{
selector.close();
}catch (Exception e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
//处理新接入的请求消息
if(key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
System.out.println("建立连接");
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ);
}
//读消息
if(key.isReadable()){
System.out.println("socket channel 数据准备完成,可以去读取");
SocketChannel sc = (SocketChannel) key.channel();
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if(readBytes>0){
//将缓冲区当前的limit设置为position,position=0,
// 用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String message = new String(bytes,"UTF-8");
System.out.println("服务器收到消息:" + message);
//处理数据
String result = "服务器响应消息:"+ message ;
//发送应答消息
doWrite(sc,result);
}
//链路已经关闭,释放资源
else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
//发送应答消息
private void doWrite(SocketChannel channel,String response)
throws IOException {
//将消息编码为字节数组
byte[] bytes = response.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//发送缓冲区的字节数组
channel.write(writeBuffer);
}
}
Reactor模式,核心流程:
注册感兴趣的事件 ->扫描是否有感兴趣的事件发生 -> 事件发生后做出相应的处理
单线程Reactor
多线程Reactor
主从多线程Reactor