Java网络编程:socket与Netty

  • 说到Java 网络编程,第一印象肯定是Netty。由于Java IO编程偏底层、不易用,Netty应运而生。Netty 并没有 Java 核心类库那些强烈的通用性、跨平台等各种负担,针对性能等特定目标以及 Linux 等特定环境,采取了一些极致的优化手段。
  • 说道网络通信,也避免不了TCP/IP协议和socket技术,下面分别讲一下,错误或不准确的地方,欢迎指正。

TCP/IP介绍

简介

  • TCP/IP传输协议,即传输控制/网络协议,也叫作网络通讯协议。它是在网络的使用中的最基本的通信协议。
  • TCP/IP传输协议对互联网中各部分进行通信的标准和方法进行了规定。并且,TCP/IP传输协议是保证网络数据信息及时、完整传输的两个重要的协议。
  • TCP/IP传输协议是严格来说是一个四层的体系结构,应用层、传输层、网络层和数据链路层都包含其中。

数据传输

  • 在网络编程中,TCP/IP统一采用big endian方式传送数据
  • 现行的计算机都是以八位一个字节为存储单位
  • little endian把低位存放到高位,而big endian把低位存放到低位.
  • 现在主流的CPU,intel系列的是采用的little endian的格式存放数据,而motorola系列的CPU采用的是big endian。

TCP粘包和黏包

现象
  • TCP粘包是指发送方发送的若干个数据包到接收方时粘成一个包。从接收缓冲区来看,后一个包数据的头紧接着前一个数据的尾
  • 当TCP连接建立后,Client发送多个报文给Server,TCP协议保证数据可靠性,但无法保证Client发了n个包,服务端也按照n个包接收。Client端发送n个数据包,Server端可能收到n-1或n+1个包
为什么出现
  • 发送方原因: TCP默认会使用Nagle算法。而Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送。所以,正是Nagle算法造成了发送方有可能造成粘包现象
  • 接收方原因: TCP接收方采用缓存方式读取数据包,一次性读取多个缓存中的数据包。自然出现前一个数据包的尾和后一个收据包的头粘到一起。
如何解决
  1. 添加特殊符号,接收方通过这个特殊符号将接收到的数据包拆分开 - DelimiterBasedFrameDecoder特殊分隔符解码器
  2. 每次发送固定长度的数据包 - FixedLengthFrameDecoder定长编码器
  3. 在消息头中定义长度字段,来标识消息的总长度 - LengthFieldBasedFrameDecoder自定义长度解码器

Socket介绍

介绍

  • socket不属于某种协议,只是网络编程技术,任何编程语言都支持socket技术开发,目的就是解决两个应用程序通讯的问题
  • Socket实现了TCP/IP,TCP/IP只是一份文档,各个不同的语言根据自己的需求对这个协议进行不同的解析,而在JAVA里对TCP/IP的解析实现就是Socket。

功能开发

  1. Socket 实现了TCP/IP协议,可以连接到网络上的终端,并收发数据

  2. ServerSocket 用于服务端,可以监听服务端口,等待客户端连接,连接后可以生成用于传输数据的Socket

  3. 创建服务端

ServerSocket serverSocket = new ServerSocket(9999);

Socket socket =  serverSocket.accept();  //等待客户端连接
  1. 创建客户端
Socket socket = new Socket("100.100.28.26", 9999);

// 注意:注册网络权限;不能在主线程做网络连接
  1. 客户端发送消息
DataOutputStream writer = new DataOutputStream(socket.getOutputStream());

writer.writeUTF("嘿嘿,你好啊,服务器..");  // 写一个UTF-8的信息
  1. 服务端接收消息
DataInputStream reader = new DataInputStream( socket.getInputStream());

String msg = reader.readUTF();
  • 也可以使用Netty很方便的实现socket编程

Netty介绍

简介

  • 按照官方定义,它是一个异步的、基于事件 Client/Server 的网络框架,目标是提供一种简单、快速构建网络应用的方式,同时保证高吞吐量、低延时、高可靠性。
  • Netty 的设计强调了 “Separation Of Concerns”,通过精巧设计的事件机制,将业务逻辑和无关技术逻辑进行隔离,并通过各种方便的抽象,一定程度上填补了了基础平台和业务开发之间的鸿沟,更有利于在应用开发中普及业界的最佳实践。
  • 从网络协议的角度,Netty 除了支持传输层的 UDP、TCP、SCTP协议,也支持 HTTP(s)、WebSocket 等多种应用层协议,它并不是单一协议的 API
  • 在应用中,需要将数据从 Java 对象转换成为各种应用协议的数据格式,或者进行反向的转换,Netty 为此提供了一系列扩展的编解码框架,与应用开发场景无缝衔接,并且性能良好。

部分重点类和方法

ServerBootstrap
  • 服务器端程序的入口,这是 Netty 为简化网络程序配置和关闭等生命周期管理,所引入的 Bootstrapping 机制。
  • 我们通常要做的创建 Channel、绑定端口、注册 Handler 等,都可以通过这个统一的入口,以Fluent API 等形式完成,相对简化了 API 使用
option / handler / attr方法
  • 都定义在AbstractBootstrap中, 所以服务端和客户端的引导类方法调用都是调用的父类的对应方法。
  • 赋值方法使用时,option / handler / attr 方法主要是针对boss线程组,childHandler / childOption / childAttr主要是针对worker线程组
  • option: 设置通道的选项参数, 对于服务端而言就是ServerSocketChannel, 客户端而言就是SocketChannel;
  • handler: 设置主通道的处理器, 对于服务端而言就是ServerSocketChannel,也就是用来处理Acceptor的操作;对于客户端的SocketChannel,主要是用来处理 业务操作;
  • attr: 设置通道的属性;
  • 对于服务端而言,有两种通道需要处理, 一种是ServerSocketChannel:用于处理用户连接的accept操作, 另一种是SocketChannel,表示对应客户端连接。
  • 以child开头的方法,都定义在ServerBootstrap中,表示处理或配置服务端接收到的对应客户端连接的SocketChannel通道。
Bootstrap
  • Bootstrap则是 Client 端的通常入口。
  • 对于客户端,一般都只有一种channel,也就是SocketChannel。
Channel

Channel,作为一个基于 NIO 的扩展框架,Channel 和 Selector 等概念仍然是 Netty 的基础组件,但是针对应用开发具体需求,提供了相对易用的抽象。

EventLoop

这是 Netty 处理事件的核心机制。我们可以使用 EventLoopGroup。我们在 NIO 中通常要做的几件事情,如注册感兴趣的事件、调度相应的 Handler 等,都是 EventLoop 负责。

ChannelFuture

这是 Netty 实现异步 IO 的基础之一,保证了同一个 Channel 操作的调用顺序。Netty 扩展了 Java 标准的 Future,提供了针对自己场景的特有Future定义。

ChannelHandler

这是应用开发者放置业务逻辑的主要地方,也是我上面提到的“Separation Of Concerns”原则的体现。

ChannelPipeline

它是 ChannelHandler 链条的容器,每个 Channel 在创建后,自动被分配一个 ChannelPipeline。在使用时,我们可以通过 ServerBootstrap 注册 ChannelInitializer,并且实现 initChannel 方法,而在该方法中则承担了向 ChannelPipleline 安装其他 Handler 的任务。

LengthFieldBasedFrameDecoder
  • 基于长度字段的帧解码器,自定义长度解决TCP粘包黏包问题
  • Netty从TCP缓冲区中读取字节, 把这些字节交给LengthFieldBasedFrameDecoder进行解码
  • 解码的操作是根据设定的规则, 根据规则,从字节中解码出来有意义的数据, 然后把数据再交给后续的Handler处理.
  • LengthFieldBasedFrameDecoder本质上是ChannelHandler,一个处理入站事件的ChannelHandler
  • LengthFieldBasedFrameDecoder需要加入ChannelPipeline中,且位于链的头部
  • 公式: 发送数据包长度 = 长度域的值 + lengthFieldOffset + lengthFieldLength + lengthAdjustment
LengthFieldBasedFrameDecoder参数
  • maxFrameLength - 发送的数据帧最大长度
  • lengthFieldOffset - 定义长度域位于发送的字节数组中的下标。换句话说:发送的字节数组中下标为${lengthFieldOffset}的地方是长度域的开始地方
  • lengthFieldLength - 用于描述定义的长度域的长度。换句话说:发送字节数组bytes时, 字节数组bytes[lengthFieldOffset, lengthFieldOffset+lengthFieldLength]域对应于的定义长度域部分
  • lengthAdjustment - 满足公式: 发送的字节数组bytes.length - lengthFieldLength = bytes[lengthFieldOffset, lengthFieldOffset+lengthFieldLength] + lengthFieldOffset + lengthAdjustment
  • initialBytesToStrip - 接收到的发送数据包,去除前initialBytesToStrip位
  • failFast - true: 读取到长度域超过maxFrameLength,就抛出一个 TooLongFrameException。false: 只有真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐