脑图:
前言:做为Netty系列第一篇文章,简要介绍学习Netty需要掌握的计算机网络知识,面试和学习中的重点TCP和UDP两个协议,并实现BIO编程对不同协议(TCP/UDP)的开发方法,同时也详细介绍了NIO编程的开发步骤和开发方法以及供读者参考。
有了本篇的基础,相信读者对学习Netty的整体架构以及原理可以很快了解与上手。
编辑
一、网络协议
1、了解OSI七层模型
口诀:巫术忘传会飙鹰,简要说明一下图片内容,网络协议一般理论上分osi7层,实现上是tcp/ip4层互联通信模型
编辑
编辑
2、了解TCP
2.1 TCP三次握手示意图:
编辑
2.2 为什么需要3次?
一、TCP是面对连接的,所以需要双方都确认连接的建立。
第一次客户端请求建立连接。第二次服务端应答客户端,并请求建立连接。第三次客户端针对服务端请求确认应答。
二、为了初始化Sequence Number的值,保证通信双方不乱序,TCP会用这个seq拼接数据。
2.3 TCP Flags:
SYN:同步序号,用于建立连接
FIN:finish标志,用于释放连接
ACK:确认序号标志
2.4 SYN超时怎么办?
SYN队列满后,通过tcp_ syncookies参数回发SYN Cookie
若为正常连接则Client会回发SYN Cookie ,直接建立连接
2.5 建立连接后, Client出现故障怎么办?
向对方发送保活探测报文,如果未收到响应则继续发送
尝试次数达到保活探测数仍未收到响应则中断连接
2.6 TCP的三次握手的漏洞 ——洪泛攻击?
三次握手中有一个第二次握手,服务端向客户端应道请求,应答请求是需要客户端IP的,服务端是需要知道客户端IP的,攻击者就伪造这个IP,往服务器端狂发送第一次握手的内容,当然第一次握手中的客户端IP地址是伪造的,从而服务端忙于进行第二次握手但是第二次握手当然没有结果,所以导致服务器端被拖累,死机。
解决方案:无效连接监控释放;延缓TCB分配方法;防火墙
2.7 TCP四次挥手示意图
编辑
四次挥手即终止TCP连接,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。
TCP连接是全双工的,因此每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。
2.8 为什么会有TIME_ WAIT状态?
确保有足够的时间让对方收到ACK包
避免新旧连接混淆
2.9 服务器出现大量CLOSE_ WAIT状态的原因?
对方关闭socket连接,我方忙于读或写,没有及时关闭连接
检查代码,特别是释放资源的代码
检查配置,特别是处理请求的线程配置
2.10 TCP的滑动窗口
TCP使用滑动窗口做流量控制与乱序重排
3.了解HTTP
3.1 HTTP报文结构
编辑编辑
3.2 一次完整http请求的过程
1、DNS域名解析(本地浏览器缓存、操作系统缓存或者DNS服务器)
2、三次握手建立 TCP 连接
3、客户端向服务器发送请求命令Get /www.xx.com/ http/1.1
4、客户端发送请求头信息
5、服务服务器应答器 Http/1.1 200 OK
6、返回响应头信息
7、服务器向客户端发送数据
8、服务器关闭 TCP 连接
3.3 HTTP状态码,五种可能的取值
1xx :指示信息--表示请求已接收,继续处理
2xx :成功--表示请求已被成功接收、理解、接受
3xx :重定向--要完成请求必须进行更进一步的操作
4xx :客户端错误--请求有语法错误或请求无法实现
5xx :服务器端错误--服务器未能实现合法的请求
二、java原生网络编程-Linux网络IO模型
BIO:进程会一直阻塞,直到数据拷贝完成
编辑
NIO:非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;
编辑
I/O多路复用:使用一个或多个固定线程来处理每一个 Socket
编辑
AIO:当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。
编辑
主要区别:主要在等待数据和数据复制这两个时间段不同
编辑
三、Java原生网络编程-BIO编程
Socket是对TCP/IP协议的抽象,是操作系统对外开放的接口
编程中的Socket是应用层与TCP/IP协议族通信的中间软件抽象层,在设计模式中,Socket其实就是一个外观模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
以下
编辑
public class TCPServer { public static void main(String[] args) throws Exception { // 创建socket,并将socket绑定到port 65000端口 ServerSocket ss = new ServerSocket(65000); // 死循环,使得socket-直等待并处理客户端发送过来的请求 while (true) { // 监听65000端口,直到客户端返回连接信息后才返回 Socket socket = ss.accept(); // 获取客户端的请求信息后,执行相关业务逻辑 new Calculator(socket).start(); } } } public class TCPClient { public static void main(String[] args) throws Exception { // 创建socket,并指定连接的是本机的端口号host:为127.0.0.1;port为65000的服务器socket Socket socket = new Socket("127.0.0.1", 65000); // 获取输出流 OutputStream os = socket.getOutputStream(); // 获取输入流 InputStream is = socket.getInputStream(); // 将要传递给server的字符串参数转换成byte数组,并数组写入到输出流中 os.write(new String("hello world").getBytes()); byte[] buff = new byte[1024]; // buff主要用来读取输入的内容,存成byte数组,ch主要用来获取读取数组的长度 int ch = is.read(buff); // 将接收流的byte数组转换成字符串,这里是从服务端回发回来的字符串参数的长度 String content = new String(buff, 0, ch); System.out.println(content); // 不要忘记关闭输入输出流以及socket is.close(); os.close(); socket.close(); } } public class Calculator extends Thread { /** * 以socket为成员变量 */ private Socket socket; public Calculator(Socket socket) { this.socket = socket; } @Override public void run() { try { // 获取socket的输出流 OutputStream outputStream = socket.getOutputStream(); // 获取socket的输入流 InputStream inputStream = socket.getInputStream(); // buff主要用来读取输入的内容,存成byte数组,ch主要用来获取读取数组 byte[] buff = new byte[1024]; int ch = inputStream.read(buff); // 将接收流的byte数组转换成字符串,这里获取的内容是客户端发送过来的字节 String content = new String(buff, 0, ch); System.out.println(content); // 往输出流里写入获得的字符串的长度,回发给客户端 outputStream.write(String.valueOf(content.length()).getBytes()); // 不要忘记关闭输入输出流以及socket inputStream.close(); outputStream.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } }
public class UDPServer { public static void main(String[] args) throws Exception { // 服务端接受客户端发送的数据报port:65001 ,监听的端口号 DatagramSocket socket = new DatagramSocket(65001); // 存储从客户端接受到的内容 byte[] buff = new byte[100]; DatagramPacket packet = new DatagramPacket(buff, buff.length); // 接受客户端发送过来的内容,并将内容封装进DatagramPacket对象中 socket.receive(packet); // 从DatagramPacket对象 中获取到真正存储的数据 byte[] data = packet.getData(); // 将数据从二进制转换成字符串形式 String content = new String(data, 0, packet.getLength()); System.out.println(content); // 将要发送给客户端的数据转换成二进制 byte[] sendedContent = String.valueOf(content.length()).getBytes(); // 服务端给客户端发送数据报 // 从DatagramPacket对象中获取到数据的来源地址与端口号 DatagramPacket packetToClient = new DatagramPacket(sendedContent, sendedContent.length, packet.getAddress(), packet.getPort()); // 发送数据给客户端 socket.send(packetToClient); } } public class UDPClient { public static void main(String[] args) throws Exception { // 客户端发数据报给服务端 DatagramSocket socket = new DatagramSocket(); // 要发送给服务端的数据 byte[] buf = "Hello World".getBytes(); // 将IP地址封装成InetAddress对象 InetAddress address = InetAddress.getByName("127.0.0.1"); // 将要发送给服务端的数据封装成DatagramPacket对象需要填写上ip地址与端口号 DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 65001); // 发送数据给服务端 socket.send(packet); // 客户端接受服务端发送过来的数据报 byte[] data = new byte[100]; // 创建DtagramPacket对象 用来存储服务端发送过来的数据 DatagramPacket receivedPacket = new DatagramPacket(data, data.length); // 将接受到的数据存储到DatagramPacket对象中 socket.receive(receivedPacket); // 将服务器端发送过来的数据取出来并打印到控制台 String content = new String(receivedPacket.getData(), 0, receivedPacket.getLength()); System.out.println(content); } }
四、Java原生网络编程-NIO编程
1、NIO开发步骤:
1、创建ServerSocketChannl,配置为非阻塞
2、绑定监听,配置tcp参数
3、创建IO线程,用于轮询多路复用器Selector
4、创建Selector。注册ServerSocketChannl
5、启动IO线程
6、当轮询到处于就绪的Channel时,判断是否为OP_ACCEPT,调用accept接收新client
7、设置新client为非zuse
8、注册SocketChannl
9、如果轮询的channel为OP_READ,则继续读取
10、如果轮询的channel为OP_WRITE,则继续发送
/** * 类说明:nio通信服务端 */ public class NioServer { private static NioServerHandle nioServerHandle; public static void start() { if (nioServerHandle != null) { nioServerHandle.stop(); } nioServerHandle = new NioServerHandle(Const.DEFAULT_PORT); new Thread(nioServerHandle, "Server").start(); } public static void main(String[] args) { start(); } } /** * 类说明:nio通信服务端处理器 */ public class NioServerHandle implements Runnable { private Selector selector; private ServerSocketChannel serverChannel; private volatile boolean started; /** * 构造方法 * * @param port 指定要监听的端口号 */ 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 (IOException 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 = Const.response(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); } } /** * 类说明:nio通信客户端 */ public class NioClient { private static NioClientHandle nioClientHandle; public static void start() { if (nioClientHandle != null) { nioClientHandle.stop(); } nioClientHandle = new NioClientHandle(Const.DEFAULT_SERVER_IP, Const.DEFAULT_PORT); new Thread(nioClientHandle, "Server").start(); } /** * 向服务器发送消息 */ public static boolean sendMsg(String msg) throws Exception { nioClientHandle.sendMsg(msg); return true; } public static void main(String[] args) throws Exception { start(); // 向服务器发送消息 Scanner scanner = new Scanner(System.in); while (NioClient.sendMsg(scanner.next())) { } } } /** * 类说明:nio通信客户端处理器 */ public class NioClientHandle implements Runnable { private String host; private int port; private volatile boolean started; // 选择器 private Selector selector; // 管道 private SocketChannel socketChannel; public NioClientHandle(String ip, int port) { this.host = ip; this.port = port; try { // 创建选择器 this.selector = Selector.open(); // 打开监听通道 socketChannel = SocketChannel.open(); // 如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式;缺省为true socketChannel.configureBlocking(false); started = true; } catch (IOException e) { e.printStackTrace(); System.exit(-1); } } public void stop() { started = false; } @Override public void run() { // 连接服务器 try { doConnect(); } catch (IOException e) { e.printStackTrace(); System.exit(-1); } // 循环遍历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(); // 我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。 // 如果我们没有删除处理过的键,那么它仍然会在事件集合中以一个激活的键出现,这会导致我们尝试再次处理它。 it.remove(); try { handleInput(key); } catch (Exception e) { if (key != null) { key.cancel(); if (key.channel() != null) { key.channel().close(); } } } } } catch (IOException e) { e.printStackTrace(); System.exit(-1); } } if (selector != null) { try { selector.close(); } catch (IOException e) { e.printStackTrace(); } } } // 具体的事件处理方法 private void handleInput(SelectionKey key) throws IOException { if (key.isValid()) { // 获得关心当前事件的channel SocketChannel sc = (SocketChannel) key.channel(); // 处理连接就绪事件 // 但是三次握手未必就成功了,所以需要等待握手完成和判断握手是否成功 if (key.isConnectable()) { // finishConnect的主要作用就是确认通道连接已建立, // 方便后续IO操作(读写)不会因连接没建立而 // 导致NotYetConnectedException异常。 if (sc.finishConnect()) { // 连接既然已经建立,当然就需要注册读事件, // 写事件一般是不需要注册的 socketChannel.register(selector, SelectionKey.OP_READ); } else { System.exit(-1); } } // 处理读事件,也就是当前有数据可读 if (key.isReadable()) { // 创建ByteBuffer,并开辟一个1k的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); // 将通道的数据读取到缓冲区,read方法返回读取到的字节数 int readBytes = sc.read(buffer); if (readBytes > 0) { buffer.flip(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); String result = new String(bytes, "UTF-8"); System.out.println("客户端收到消息:" + result); } // 链路已经关闭,释放资源 else if (readBytes < 0) { key.cancel(); sc.close(); } } } } /** * 进行连接 */ private void doConnect() throws IOException { // 如果此通道处于非阻塞模式,则调用此方法将启动非阻塞连接操作。 // 如果连接马上建立成功,则此方法返回true。 // 否则,此方法返回false, // 因此我们必须关注连接就绪事件, // 并通过调用finishConnect方法完成连接操作。 if (socketChannel.connect(new InetSocketAddress(host, port))) { // 连接成功,关注读事件 socketChannel.register(selector, SelectionKey.OP_READ); } else { socketChannel.register(selector, SelectionKey.OP_CONNECT); } } /** * 写数据对外暴露的API * * @param msg * @throws IOException */ public void sendMsg(String msg) throws IOException { doWrite(socketChannel, msg); } /** * 写数据 * * @param sc * @param request * @throws IOException */ private void doWrite(SocketChannel sc, String request) throws IOException { byte[] bytes = request.getBytes(); ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); writeBuffer.put(bytes); writeBuffer.flip(); sc.write(writeBuffer); } } public class Const { public static int DEFAULT_PORT = 12345; public static String DEFAULT_SERVER_IP = "127.0.0.1"; public static String response(String msg) { return "Hello," + msg + ",Now is " + new java.util.Date(System.currentTimeMillis()).toString(); } }
2、Java原生网络编程-BIO、NIO主要区别
阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
面向流与面向缓冲
BIO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。