手寫web服務器:基于NIO重構服務器,實現(xiàn)post請求處理

前言
前幾天一直被post請求處理的問題卡著,因此web服務器這邊也沒啥進展,再加上昨天又突然被告知要去加班,所以這個問題就一直被一次次往后拖,還好今天有時間,就抽空把這個問題徹底解決了,然后服務這邊也徹底從原來的socket,被我重構成Nio的ServerSocketChannel,也就是我們前面說的非阻塞式socket,今天主要介紹整個重構過程,nio的知識點暫時也不打算講,因為我也沒有搞得特別清楚。好了,話不多說,直接重構。
重構
手寫我們重新寫了sokcet的核心程序,實現(xiàn)方式徹底改變了,首先是一個服務器接收客戶端請求的線程:
接收服務器請求線程
static class AcceptSocketThread extends Thread {
volatile boolean runningFlag = true;
@Override
public void run() {
try {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(30000));
serverChannel.configureBlocking(false);
while (runningFlag) {
SocketChannel channel = serverChannel.accept();
if (null == channel) {
logger.info("服務端監(jiān)聽中.....");
} else {
channel.configureBlocking(false);
logger.info("一個客戶端上線,占用端口 :{}", channel.socket().getPort());
keys.put(channel.socket().getPort(), channel);
new ResponseThread().start();
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
在線程內(nèi)部,我們通過ServerSocketChannel.open創(chuàng)建了一個ServerSocketChannel通信頻道,并設置頻道端口是30000;
configureBlocking是設置當前通信是否阻塞,這里我們設置的是false,也就是非阻塞通信;
然后通過一個死循環(huán)監(jiān)聽服務器serverChannel是否被連接,這里serverChannel.accept()返回值為null表示未建立連接或者連接被關閉;
如果建立連接,我們將通信頻道放進keys通信頻道隊列中:
public static volatile Map<Integer, SocketChannel> keys =
Collections.synchronizedMap(new HashMap<>());
并啟動一個響應請求線程去處理這個頻道中的請求,下面我們看處理線程
處理請求線程
在寫這些文字時候,我發(fā)現(xiàn)這里其實沒必要創(chuàng)建隊列存放會話頻道,可以直接把這塊的隊列傳進線程,并處理(因為我這塊代碼是參考別人的,然后進行了大改,后面還需要進一步優(yōu)化)
/**
* 處理客戶端請求
*/
static class ResponseThread extends Thread {
ByteBuffer buffer = ByteBuffer.allocate(1024);
@Override
public void run() {
int num = 0;
Iterator<Integer> ite = keys.keySet().iterator();
while (ite.hasNext()) {
int key = ite.next();
StringBuffer stb = new StringBuffer();
try {
SocketChannel socketChannel = keys.get(key);
if (Objects.isNull(socketChannel)) {
break;
}
while ((num = socketChannel.read(buffer)) > 0) {
buffer.flip();
stb.append(charset.decode(buffer).toString());
buffer.clear();
}
if (stb.length() > 0) {
MsgWrapper msg = new MsgWrapper();
msg.key = key;
msg.msg = stb.toString();
logger.info("端口:{}的通道,讀取到的數(shù)據(jù):{}",msg.key, msg.msg);
msgQueue.add(msg);
threadPoolExecutor.execute(new SyskeRequestNioHandler(socketChannel, msg.msg));
ite.remove();
}
} catch (Exception e) {
ite.remove();
logger.error("error: 端口占用為:{},的連接的客戶端下線了", keys.get(key).socket().getPort(), e);
}
}
logger.info("讀取線程監(jiān)聽中......");
}
}
因為原代碼,作者的接收線程、處理線程都是在main方法啟動的,所以他這樣定義是ok的,但我這里其實就沒必要了。
看了上面的代碼,大家會發(fā)現(xiàn),nio中不再有InputStream或者OutputStream這樣的類,這是因為nio的底層實現(xiàn)采用了新的架構,有一個selector進行頻道管理,當某個頻道有數(shù)據(jù)進來的時候,selector會切換到這個頻道進行數(shù)據(jù)處理,如果沒有數(shù)據(jù)他會去處理其他頻道的數(shù)據(jù),不像我們之前的I/O,一次通信就一個管道,沒有數(shù)據(jù)就一直等待,所以也就不會導致阻塞。
我覺得有個例子能很好地說明這兩種模型,傳統(tǒng)的I/o就好比一個單位的電話,電話雖然很多,但是線路只有一條,同時只能有一個電話進行通話,電話不斷,其他人根本就打不進去,也沒法接電話,只能等著這個接收電話的人打完電話;
Nio就相當于這個單位為了解決同時只能有一個人打電話這種情況,專門雇了一個接線員負責線路切換,當有電話進來以后,接線員會把對應的電話借給對應的人,這樣即提高了線路的效率,也避免了阻塞的情況。

做完上面的改動后,我們的post請求就不再阻塞了,然后我們還優(yōu)化了request的初始化。
優(yōu)化請求初始化
現(xiàn)在不論get請求,還是post請求,最終都會拿到一個純文本的請求參數(shù),然后我我把它分別處理成header(請求方法、請求地址)、requestAttributeMap(請求頭參數(shù))、requestBody(請求體):
private void initRequest() throws IllegalParameterException{
logger.info("SyskeRequest start init");
String[] inputs = input.split("\r\n");
System.out.println(Arrays.toString(inputs));
Map<String, Object> attributeMap = Maps.newHashMap();
boolean hasBanlk = false;
StringBuilder requestBodyBuilder = new StringBuilder();
for (int i = 0; i < inputs.length; i++) {
if(i == 0) {
String[] headers = inputs[0].split(" ", 3);
String requestMapping = headers[1];
if (requestMapping.contains("?")) {
int endIndex = requestMapping.lastIndexOf('?');
String requestParameterStr = requestMapping.substring(endIndex + 1);
requestMapping = requestMapping.substring(0, endIndex);
String[] split = requestParameterStr.split("&");
for (String s : split) {
String[] split1 = s.split("=");
attributeMap.put(StringUtil.trim(split1[0]), StringUtil.trim(split1[1]));
}
}
this.header = new RequestHear(RequestMethod.match(headers[0]), requestMapping);
} else {
if (StringUtil.isEmpty(inputs[i])) {
hasBanlk = true;
}
if (inputs[i].contains(":") && Objects.equals(hasBanlk, Boolean.FALSE)) {
String[] split = inputs[i].split(":", 2);
attributeMap.put(split[0], split[1]);
} else {
// post 請求
requestBodyBuilder.append(inputs[i]);
}
}
}
requestAttributeMap = attributeMap;
requestBody = JSON.parseObject(requestBodyBuilder.toString());
logger.info("requestBodyBuilder: {}", requestBodyBuilder.toString());
logger.info("SyskeRequest init finished. header: {}, requestAttributeMap: {}", header, requestAttributeMap);
}
這里就很簡單了,就是通過\r\n分割即可。
get和post唯一的區(qū)別就是,get請求的參數(shù)都在requestAttributeMap,而post的請求參數(shù)在requestBody。
/**
* 處理post請求
* @param method
* @return
* @throws IllegalAccessException
* @throws InstantiationException
* @throws InvocationTargetException
*/
private Object doPost(Method method) throws IllegalAccessException, InstantiationException, InvocationTargetException {
JSONObject requestBody = (JSONObject)request.getRequestBody();
return doRequest(method, requestBody);
}
/**
* 處理get請求
* @param method
* @param requestAttributeMap
* @return
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private Object doGet(Method method, Map<String, Object> requestAttributeMap) throws InvocationTargetException, IllegalAccessException, InstantiationException {
return doRequest(method, requestAttributeMap);
}
測試
然后我們測試下看下,這里就只測試post了,這里我用的postMan:

看下后臺:

請求體已經(jīng)有數(shù)據(jù)了,后面的就很簡單了
總結
我現(xiàn)在越來越覺得,作為一個web后端工程師,網(wǎng)絡編程是一個特別重要的技能,因為你不了解數(shù)據(jù)在網(wǎng)絡中的傳輸過程,不了解各種協(xié)議,不了解各種請求頭,那你再遇到具體問題的時候,是根本沒有任何思路的。
可能在你眼里你可能會覺得一切你解決不了的問題,都是玄學問題,但事實并非如此。
所以,對我現(xiàn)在而言,學習的方向大概分為這幾種:
多線程:這個應該是一個比較核心,掌握的好,你的工作真的會事半功倍的 網(wǎng)絡編程:這個原因我前面說了 算法,包括數(shù)據(jù)結構等:幫助你構建更好的模型,讓你的程序運行更快,性能更好 虛擬化相關知識,比如 docker、k8s等,以及jenkins自動化構建,這一塊現(xiàn)在是比較主流的技術主流開源框架學習,這里我會花比較少的時間,以搞清楚具體的原理和實現(xiàn)方式為目的
今天把這個問題解決了,后面又可以繼續(xù)實現(xiàn)springboot的其他注解了,繼續(xù)搞事情。好了,今天就到這里吧!
下面是項目的開源倉庫,有興趣的小伙伴可以去看看,如果有想法的小伙伴,我真心推薦你自己動個手,自己寫一下,真的感覺不錯:
https://github.com/Syske/syske-boot

