本來想寫關(guān)于netty類的時序圖,學(xué)習(xí)下設(shè)計模式并學(xué)習(xí)如何擴展Java nio的,畢竟對于我這種擰螺絲釘?shù)慕o我一個任務(wù)如何寫出高內(nèi)聚低耦合的代碼才是重要的,但是找不到合適相關(guān)聯(lián)Java NIO和netty相關(guān)的代碼,所以我花費了一點時間整理了下相關(guān)代碼。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class NioSocketServer {
private static final int NCPU = Runtime.getRuntime().availableProcessors();
private static final int BUFFERSIZE = 1024;
private final ServerSocketChannel serverSocketChannel;
//簡易的Reactor模型,一個boss線程,2倍核數(shù)的工作線程
private final Selector bossselector;
private final Work[] works;
private AtomicInteger index = new AtomicInteger();
//用于緩存每個客戶端粘包拆包等數(shù)據(jù).
private Map<SocketAddress,Read> cacheChannelBuffer = new ConcurrentHashMap<>();
public NioSocketServer()throws IOException{
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8888));
bossselector = Selector.open();
SelectionKey selectionKey = serverSocketChannel.register(bossselector, SelectionKey.OP_ACCEPT);
new Thread(new Boss()).start();
works = new Work[NCPU * 2];
for(int i = 0;i < works.length;i++){
new Thread(works[i] = new Work(Selector.open(),i)).start();
}
}
public void accept(SelectionKey key) {
System.out.println("accept事件");
try {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
int i = index.getAndIncrement() & works.length - 1;
works[i].register(socketChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
//包協(xié)議:包=包頭(4byte)+包體,包頭內(nèi)容為包體的數(shù)據(jù)長度
public void read(SelectionKey selectionKey) {
System.out.println("read事件");
try {
SocketChannel channel = (SocketChannel) selectionKey.channel();
SocketAddress address = channel.getRemoteAddress();
Read read = cacheChannelBuffer.get(address);
int bodyLen = -1;
ByteBuffer byteBuffer;
if(read == null){
byteBuffer = ByteBuffer.allocate(BUFFERSIZE);
}else{
if(read.headerLength == -1){
byteBuffer = ByteBuffer.allocate(BUFFERSIZE);
}else{
bodyLen = read.getHeaderLength();
byteBuffer = ByteBuffer.allocate(read.getHeaderLength());
}
ByteBuffer readByteBuffer = read.getByteBuffer();
if(readByteBuffer != null && readByteBuffer.hasRemaining()){
readByteBuffer.flip();
byteBuffer.put(readByteBuffer);
}
read.setByteBuffer(null);
read.setHeaderLength(-1);
}
channel.read(byteBuffer);
byteBuffer.flip();
while (byteBuffer.remaining() > 0) {
if (bodyLen == -1) {// 還沒有讀出包頭,先讀出包頭
if (byteBuffer.remaining() >= 4) {// 讀出包頭,否則緩存
byteBuffer.mark();
bodyLen = byteBuffer.getInt();
} else {
remaining(read, byteBuffer, address, bodyLen);
break;
}
} else {// 已經(jīng)讀出包頭
if (byteBuffer.remaining() >= bodyLen) {// 大于等于一個包,否則緩存
byte[] bodyByte = new byte[bodyLen];
byteBuffer.get(bodyByte, 0, bodyLen);
bodyLen = -1;
System.out.println("receive from clien content is:" + new String(bodyByte));
} else {
remaining(read, byteBuffer, address, bodyLen);
break;
}
}
}
// String httpResponse = "HTTP/1.1 200 OK\r\n" + "Content-Length: 38\r\n" + "Content-Type: text/html\r\n" + "<html><body>Hello World!</body></html>";
// ByteBuffer buffer = ByteBuffer.wrap(httpResponse.getBytes());
// int len = channel.write(buffer);
// if (len < 0){
// throw new IllegalArgumentException();
// }
// if (len == 0) {
// selectionKey.interestOps(SelectionKey.OP_WRITE);
// }
selectionKey.interestOps(SelectionKey.OP_READ);
} catch (Exception e) {
try {
selectionKey.cancel();
serverSocketChannel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
}
private void remaining(Read read,ByteBuffer byteBuffer,SocketAddress address,int bodyLen){
if(!byteBuffer.hasRemaining()){
return;
}
if(read == null){
read = new Read();
cacheChannelBuffer.put(address,read);
}
read.setHeaderLength(bodyLen);
int remaining = byteBuffer.remaining();
byte[] remainingByte = new byte[remaining];
byteBuffer.get(remainingByte, 0, remaining);
read.setByteBuffer(ByteBuffer.allocate(remaining).put(remainingByte));
}
public void write(SelectionKey selectionKey) {
System.out.println("write事件");
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
String httpResponse = "HTTP/1.1 200 OK\r\n" + "Content-Length: 38\r\n" + "Content-Type: text/html\r\n" + "<html><body>Hello World!</body></html>";
System.out.println("response from server to client");
try {
ByteBuffer byteBuffer = ByteBuffer.wrap(httpResponse.getBytes());
while (byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}
selectionKey.interestOps(SelectionKey.OP_READ);
} catch (IOException e) {
try {
selectionKey.cancel();
serverSocketChannel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
}
class Boss implements Runnable{
private volatile boolean isExit;
public boolean isExit() {
return isExit;
}
public void setExit(boolean exit) {
isExit = exit;
}
@Override
public void run(){
try {
while (!isExit) {
int selectKey = bossselector.select();
if (selectKey > 0) {
Set<SelectionKey> keySet = bossselector.selectedKeys();
Iterator<SelectionKey> iter = keySet.iterator();
while (iter.hasNext()) {
SelectionKey selectionKey = iter.next();
iter.remove();
if (selectionKey.isAcceptable()) {
accept(selectionKey);
} else {
System.out.println("boss線程不可能出現(xiàn)work線程的事件,請檢查代碼。");
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
}
}
}
class Work implements Runnable{
private final Selector workSelector;
private final int i;
private volatile boolean isExit ;
public Work(Selector workSelector,int i){
this.workSelector = workSelector;
this.i = i;
}
public SelectionKey register(SocketChannel socketChannel) throws ClosedChannelException {
return socketChannel.register(workSelector, SelectionKey.OP_READ);
}
public boolean isExit() {
return isExit;
}
public void setExit(boolean exit) {
isExit = exit;
}
@Override
public void run(){
try {
while (!isExit) {
int selectKey = workSelector.select(10);
if (selectKey > 0) {
Set<SelectionKey> keySet = workSelector.selectedKeys();
Iterator<SelectionKey> iter = keySet.iterator();
while (iter.hasNext()) {
SelectionKey selectionKey = iter.next();
iter.remove();
if (selectionKey.isAcceptable()) {
System.out.println("work線程不可能出現(xiàn)boss線程的事件,請檢查代碼。");
}else if (selectionKey.isReadable()) {
read(selectionKey);
}else if (selectionKey.isWritable()) {
write(selectionKey);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
}
}
}
class Read{
private ByteBuffer byteBuffer;
private int headerLength = -1;
public ByteBuffer getByteBuffer() {
return byteBuffer;
}
public void setByteBuffer(ByteBuffer byteBuffer) {
this.byteBuffer = byteBuffer;
}
public int getHeaderLength() {
return headerLength;
}
public void setHeaderLength(int headerLength) {
this.headerLength = headerLength;
}
}
public static void main(String args[]) throws IOException {
NioSocketServer server = new NioSocketServer();
}
}
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.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NioSocketClient {
private SocketChannel socketChannel;
private Selector selector = null;
public NioSocketClient() throws IOException{
InetSocketAddress inetSocketAddress = new InetSocketAddress(8888);
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(inetSocketAddress);
socketChannel.register(selector, SelectionKey.OP_CONNECT);
new Thread(new Work()).start();
}
public void finishConnect(SelectionKey key) {
System.out.println("client finish connect!");
SocketChannel socketChannel = (SocketChannel) key.channel();
try {
socketChannel.finishConnect();
key.interestOps(SelectionKey.OP_WRITE);
} catch (IOException e) {
e.printStackTrace();
}
}
public void read(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = channel.read(byteBuffer);
if (len > 0) {
byteBuffer.flip();
byte[] byteArray = new byte[byteBuffer.limit()];
byteBuffer.get(byteArray);
System.out.println("client receive from server,content:"+new String(byteArray));
len = channel.read(byteBuffer);
byteBuffer.clear();
}
key.interestOps(SelectionKey.OP_READ);
}
public void send(SelectionKey key) {
SocketChannel channel = (SocketChannel) key.channel();
for (int i = 0; i < 10; i++) {
String ss = i + "Server ,how are you ? this is package message from NioSocketClient!";
int headSize = (ss).getBytes().length;
ByteBuffer byteBuffer = ByteBuffer.allocate(4 + headSize);
byteBuffer.putInt(headSize);
byteBuffer.put(ss.getBytes());
byteBuffer.flip();
System.out.println("client send:" + i + ",context:" + ss);
while (byteBuffer.hasRemaining()) {
try {
channel.write(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
key.interestOps(SelectionKey.OP_READ);
}
class Work implements Runnable{
@Override
public void run(){
while (true) {
try {
int key = selector.select();
if (key > 0) {
Set<SelectionKey> keySet = selector.selectedKeys();
Iterator<SelectionKey> iter = keySet.iterator();
while (iter.hasNext()) {
SelectionKey selectionKey = null;
synchronized (iter) {
selectionKey = iter.next();
iter.remove();
}
if (selectionKey.isConnectable()) {
finishConnect(selectionKey);
}
if (selectionKey.isWritable()) {
send(selectionKey);
}
if (selectionKey.isReadable()) {
read(selectionKey);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String args[]) throws IOException {
NioSocketClient client = new NioSocketClient();
}
}
以上服務(wù)端實現(xiàn)了簡易的Reactor模型,并自定義了通信協(xié)議(協(xié)議格式:長度+內(nèi)容),并實現(xiàn)了粘包和拆包的邏輯。netty不管怎么封裝,最終都是要封裝NIO那幾行代碼。
以下是我遇到的問題和我覺得需要注意細(xì)節(jié)。
1.我是在win10 jdk1.8實現(xiàn)的代碼,服務(wù)端worker線程獲取數(shù)據(jù)采用select()時,當(dāng)有新的客戶端連接時,獲取不到數(shù)據(jù),采用select(long timeout)可以獲取到數(shù)據(jù),這個問題沒找到原因。
2.Java底層無法得知channel獲取了多少數(shù)據(jù),所以需要自定義ByteBuffer的大小,在發(fā)生拆包粘包時需要注意。netty實現(xiàn)了自動實現(xiàn)計算ByteBuffer的大小,不一定準(zhǔn)確。
3.自定義的nio代碼中,很少看到OP_WRITE的處理,經(jīng)??吹降拇a就是在請求處理完成后,直接通過下面的代碼將結(jié)果返回給客戶端。什么時候采用OP_WRITE,引用別人的一段話:
如果客戶端的網(wǎng)絡(luò)或者是中間交換機的問題,使得網(wǎng)絡(luò)傳輸?shù)男屎艿?,這時候會出現(xiàn)服務(wù)器已經(jīng)準(zhǔn)備好的返回結(jié)果無法通過TCP/IP層傳輸?shù)娇蛻舳?。這時候在執(zhí)行上面這段程序的時候就會出現(xiàn)以下情況。
(1) bb.hasRemaining()一直為“true”,因為服務(wù)器的返回結(jié)果已經(jīng)準(zhǔn)備好了。
(2) socketChannel.write(bb)的結(jié)果一直為0,因為由于網(wǎng)絡(luò)原因數(shù)據(jù)一直傳不過去。
(3) 因為是異步非阻塞的方式,socketChannel.write(bb)不會被阻塞,立刻被返回。
(4) 在一段時間內(nèi),這段代碼會被無休止地快速執(zhí)行著,消耗著大量的CPU的資源。事實上什么具體的任務(wù)也沒有做,一直到網(wǎng)絡(luò)允許當(dāng)前的數(shù)據(jù)傳送出去為止。
因此,要對OP_WRITE加以處理,常用用法為:
String httpResponse = "HTTP/1.1 200 OK\r\n" + "Content-Length: 38\r\n" + "Content-Type: text/html\r\n" + "<html><body>Hello World!</body></html>";
ByteBuffer buffer = ByteBuffer.wrap(httpResponse.getBytes());
int len = channel.write(buffer);
if (len < 0){
throw new IllegalArgumentException();
}
if (len == 0) {
selectionKey.interestOps(SelectionKey.OP_WRITE);
}
以上這段話在我實現(xiàn)的服務(wù)端中屏蔽了,后續(xù)會講解netty時序圖,學(xué)習(xí)netty優(yōu)秀的源碼。
最后,目前在找工作,現(xiàn)在在家cha