網絡通信其實就是幹這麼幾件事
1.建立連接
2.客戶端發送數據
3.服務器接受到數據,然後根據數據內容進行處理,然後返回數據
4.不停重複2,3
5.關閉連接
我們爲什麼要用netty來搭建服務器呢?或者說netty爲什麼成爲了高人氣的服務器框架呢?
下面我們就來仔細研究一下吧。
在網絡編程中,我們至少得有一臺服務器,有一臺客戶端,兩者建立連接,然後進行通信。
在早期,我們的服務器都是採用bio的方式進行交流的。
BIO服務器
//服務器端代碼
public class Server {
protected static final String ip = "127.0.0.1";
protected static final int port = 6010;
private static void start() {
try {
ServerSocket server = new ServerSocket(port);//1
while(true) {
Socket client = server.accept();//2
System.out.println("accept");
new Thread(new ServerHandler(client)).start();//3
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
start();
}
}
//服務器處理邏輯
public class ServerHandler implements Runnable {
private Socket client;
public ServerHandler(Socket client) {
this.client = client;
}
public void run() {
byte[] buffer = new byte[1024];
while (true) {
int read = 0;
try {
read = client.getInputStream().read(buffer);
if (!(read > 0)) break;
System.out.println(new String(buffer,0, read));
client.getOutputStream().write(buffer,0,read);
} catch (IOException e) {
e.printStackTrace();
return;
}
}
}
}
//客戶端代碼
public class Client {
public static void main(String[] args) {
start();
}
private static void start() {
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(Server.ip, Server.port));
Thread.sleep(1000);//模擬客戶端阻塞
socket.getOutputStream().write("hello world".getBytes());
byte[] buffer = new byte[1024];
while (socket.getInputStream().read(buffer) > 0) {
System.out.println(new String(buffer));
buffer = new byte[1024];
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
特點
上面這就是用BIO實現的服務器和客戶端通信,可見服務器和客戶端之間是能正常通信,處理業務的,人們在實踐中發現,大量的連接其實不是一直在傳輸消息的,而是阻塞狀態。但是以BIO的方式來做的話,每一個客戶端連接上來都需要創建一個線程來與客戶端保持連接。當有大量連接時,服務器需要浪費大量的內存來保持這些連接(分配給線程)。
這個時候出現了NIO技術,即在請求數據的時候,線程不再阻塞了。
如果數據沒有準備好,那麼當線程請求時會直接收到沒有準備好的信號。當然準備好的話,就會收到準備好了的信號。
所以我們就能用一個線程,將全部的連接保存下來,然後挨個去輪詢,當找到某個連接數據準備好後就新建線程來處理這個連個連接。
這個時候我們就不需要在新建大量線程去保持連接了,只需要一個或者幾個(當連接數太多的時候,可以適當多幾個)就可以維持大量的連接,又因爲大量的連接其實是未準備好的,所以處理的連接數據的線程數量不會太多。
在此基礎上,操作系統提供了select,poll,epoll來處理上面保持連接的問題。當這些連接準備好你註冊好的事件的時候,就會返回,你能拿到與之對應的連接,然後通過連接拿到數據進行處理。這就是java的NIO的本質。
NIO服務器
//NIO實現的服務器端代碼
public class Server {
protected static final String ip = "127.0.0.1";
protected static final int port = 6010;
public static void main(String[] args) {
start();
}
private static void start() {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(ip, port));
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
selector.select(); //阻塞方法 若返回則說明有 感興趣的事件準備好了
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
while (keyIterator.hasNext()) {//處理客戶端連接
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
ssc = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = ssc.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("有客戶端連接!");
}
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
readBuffer.clear();//清除緩衝區
int numRead;
try{
numRead = socketChannel.read(readBuffer);
if (numRead == -1) {
socketChannel.close();
key.cancel();
continue;
}
}catch (IOException e){
key.cancel();
socketChannel.close();
continue;
}
String msg = new String(readBuffer.array(),0,numRead);
System.out.println("接收到消息" + msg);
socketChannel.register(selector,SelectionKey.OP_WRITE);
}
if (key.isWritable()) {
String msg = "hello";
SocketChannel channel = (SocketChannel) key.channel();
System.out.println("發送消息:" + msg);
sendBuffer.clear();
sendBuffer.put(msg.getBytes());
sendBuffer.flip();//由寫變爲讀,反轉
channel.write(sendBuffer);
channel.register(selector,SelectionKey.OP_READ); //註冊讀操作
}
keyIterator.remove(); //移除當前的key
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//客戶端
public class Client {
protected static final String ip = "127.0.0.1";
protected static final int port = 6010;
public static void main(String[] args) {
start();
}
private static void start() {
try {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress(ip, port));//建立連接
ByteBuffer readBuffer = ByteBuffer.allocate(1024);//緩衝區
ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
sendBuffer.put("hello".getBytes());//將要發送的數據寫入buffer
sc.write(sendBuffer);//發送消息
int length = sc.read(readBuffer);//讀取信息
String s = new String(readBuffer.array(), 0, length);
System.out.println(s);
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客戶端可以用BIO的客戶端,也可以NIO的SocketChannel來實現。
運行效果如下
用原生NIO來寫代碼實在是太過繁瑣,而且很容易出現一些不知名的bug(因爲知識的漏洞)。所以有人將原生NIO進行的封裝,netty這個項目就此誕生了。
Netty 服務器
用netty來實現服務器和客戶端
//服務器端
public class NettyServer {
private static final String IP = "127.0.0.1";
private static final int PORT = 6010;
private static final int BOSSNUM = Runtime.getRuntime().availableProcessors() * 2;
private static final int WKNUM = 100;
private static final EventLoopGroup bossGroup = new NioEventLoopGroup(BOSSNUM);
private static final EventLoopGroup workGroup = new NioEventLoopGroup(WKNUM);
public static void start() throws InterruptedException {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new TCPChannelHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(IP, PORT).sync();
channelFuture.channel().closeFuture().sync();
System.out.println("start");
}
public static void main(String[] args) throws InterruptedException {
start();
}
}
//服務器端業務處理代碼
public class TCPChannelHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Channel Active");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("收到" + msg);
ctx.channel().writeAndFlush("Hello Clent");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
//客戶端
public class NettyClient implements Runnable{
public void run() {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LengthFieldPrepender(4));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new TCPClient());
}
});
try {
ChannelFuture sync = bootstrap.connect("127.0.0.1", 6010).sync();
sync.channel().writeAndFlush("hello");
Thread.sleep(100);
group.shutdownGracefully();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread thread = new Thread(new NettyClient());
thread.start();
}
}
//客戶端業務邏輯處理代碼
public class TCPClient extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("active");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("receive: " + msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
Netty封裝了NIO,降低了NIO實現網絡編程的難度,對比代碼量就可以看出。而且在代碼的可讀性上也大大提高了。
小結
不是說NIO一定比BIO好,只是說現在網絡環境是連接數量多,傳輸數據量少(瀏覽網頁)爲主,NIO更加適合,假如你的服務器連接人數不多,又是傳輸數據量比較大的長連接的話,那麼BIO肯定比NIO更加適合你。
我說這句話的目的是希望大家不會要盲目推崇NIO,具體問題具體分析。
而Netty是一個基於多路複用io模型的NIO網絡編程框架,屏蔽了很多NIO中的陷進,能更加容易的搭建自己的服務器(不一定是http服務器,可以自己實現自己的協議。比如dubbo)。
下一篇正式進入Netty的學習。