שרת HTTP עם Netty
1. סקירה כללית
במדריך זה אנו הולכים ליישם שרת פשוט עם מעטפת עליונה HTTP עם Netty, מסגרת אסינכרונית המעניקה לנו את הגמישות לפתח יישומי רשת בג'אווה.
2. Bootstrapping בשרת
לפני שנתחיל, עלינו להיות מודעים למושגי היסוד של Netty, כגון ערוץ, מטפל, מקודד ומפענח.
כאן נקפוץ ישר לאתחול השרת, שהוא לרוב זהה לשרת פרוטוקולים פשוט:
מחלקה ציבורית HttpServer {יציאת פרטי פרטית; לוגר לוגר סטטי פרטי = LoggerFactory.getLogger (HttpServer.class); // קונסטרוקטור // שיטה עיקרית, כמו שרת פרוטוקול פשוט ריק ריק () זורק חריג {... ServerBootstrap b = ServerBootstrap חדש (); b.group (bossGroup, workerGroup) .channel (NioServerSocketChannel.class) .handler (LoggingHandler new (LogLevel.INFO)) .childHandler (ChannelInitializer חדש () {@Override מוגן בטל initChannel (SocketChannel ch) זורק חריג {ChannelPipeline p = ch .pipeline (); p.addLast (HttpRequestDecoder חדש ()); p.addLast (HttpResponseEncoder חדש ()); p.addLast (CustomHttpServerHandler חדש ());}}); ...}}
אז כאן רק ה childHandler שונה בהתאם לפרוטוקול שאנו רוצים ליישם, שהוא HTTP עבורנו.
אנו מוסיפים לצינור השרת שלושה מטפלים:
- של נטי HttpResponseEncoder - לסידור
- של נטי HttpRequestDecoder - לעריקת ייעול
- שלנו CustomHttpServerHandler - להגדרת התנהגות השרת שלנו
בואו נסתכל בפירוט על המטפל האחרון בהמשך.
3. CustomHttpServerHandler
תפקיד המטפל המותאם אישית שלנו הוא לעבד נתונים נכנסים ולשלוח תגובה.
בואו נשבר את זה כדי להבין את פעולתו.
3.1. מבנה המטפל
CustomHttpServerHandler מרחיב את התקציר של נטי SimpleChannelInboundHandler ומיישם את שיטות מחזור החיים שלה: כשם השיטה מרמז, channelReadComplete שוטף את הקשר המטפל לאחר שההודעה האחרונה בערוץ נצרכה כך שהיא תהיה זמינה להודעה הנכנסת הבאה. השיטה יוצא מן הכלל מיועד לטיפול בחריגים אם בכלל. עד כה, כל מה שראינו הוא קוד ה- boilerplate. עכשיו בואו נמשיך עם הדברים המעניינים, היישום של channelRead0. מקרה השימוש שלנו פשוט, השרת פשוט יהפוך את גוף הבקשה ופרמטרי השאילתה, אם בכלל, לרישיות. מילת אזהרה כאן לגבי שיקוף נתוני בקשה בתגובה - אנו עושים זאת רק למטרות הדגמה, כדי להבין כיצד אנו יכולים להשתמש ב- Netty כדי להטמיע שרת HTTP. פה, אנו נצרוך את ההודעה או הבקשה ונגדיר את תגובתה בהתאם להמלצת הפרוטוקול (ציין זאת RequestUtils הוא משהו שנכתוב רק בעוד רגע): כפי שאנו רואים, כאשר הערוץ שלנו מקבל HttpRequest, היא בודקת תחילה אם הבקשה מצפה לסטטוס 100 המשך. במקרה זה, אנו מיד כותבים חזרה עם תגובה ריקה עם סטטוס לְהַמשִׁיך: לאחר מכן, המטפל מאותחל מחרוזת שתישלח כתגובה ומוסיף אליה את פרמטרי השאילתה של הבקשה כדי להישלח בחזרה כמות שהיא. בואו נגדיר את השיטה formatParams והניחו אותו ב- RequestUtils כיתת עוזר לעשות זאת: לאחר מכן, בקבלת Http תוכן, אנו לוקחים את גוף הבקשה וממירים אותו לאותיות רישיות: כמו כן, אם קיבל Http תוכן הוא LastHttpContent, אנו מוסיפים הודעת פרידה וכותרות נגררות, אם יש: עכשיו שהנתונים שלנו שישלחו מוכנים, נוכל לכתוב את התגובה ל- ChannelHandlerContext: בשיטה זו יצרנו a FullHttpResponse עם גרסת HTTP / 1.1, הוספת הנתונים שהכנו קודם. אם יש להשאיר בקשה בחיים, או במילים אחרות, אם הקשר לא אמור להיסגר, הגדרנו את התגובה חיבור כותרת כ להשאיר בחיים. אחרת, אנו סוגרים את הקשר. כדי לבדוק את השרת שלנו, בוא נשלח כמה פקודות cURL ונראה את התגובות. כמובן, עלינו להפעיל את השרת על ידי הפעלת הכיתה HttpServer לפני זה. בואו נפעיל תחילה את השרת ונספק קובץ cookie עם הבקשה: כתגובה, אנו מקבלים: אנחנו יכולים גם להכות //127.0.0.1:8080?param1=one מכל דפדפן כדי לראות את אותה התוצאה. כמבחן השני שלנו, בואו נשלח POST עם גוף תוכן לדוגמא: הנה התגובה: הפעם, מכיוון שבקשתנו הכילה גוף, השרת שלח אותו חזרה באותיות רישיות. במדריך זה ראינו כיצד ליישם את פרוטוקול HTTP, במיוחד שרת HTTP המשתמש ב- Netty. HTTP / 2 ב- Netty מדגים יישום שרת לקוח של פרוטוקול HTTP / 2. כמו תמיד, קוד המקור זמין ב- GitHub.מחלקה ציבורית CustomHttpServerHandler מרחיב את SimpleChannelInboundHandler {בקשה HttpRequest פרטית; StringBuilder responseData = StringBuilder חדש (); @ Override public void channelReadComplete (ChannelHandlerContext ctx) {ctx.flush (); } @Override מוגן חלל channelRead0 (ChannelHandlerContext ctx, Object msg) {// יישום לעקוב} @Override public void exceptionCaught (ChannelHandlerContext ctx, Throwable סיבה) {cause.printStackTrace (); ctx.close (); }}
3.2. קורא את הערוץ
אם (msg מופע של HttpRequest) {HttpRequest בקשה = this.request = (HttpRequest) msg; אם (HttpUtil.is100ContinueExpected (בקשה)) {writeResponse (ctx); } responseData.setLength (0); responseData.append (RequestUtils.formatParams (בקשה)); } responseData.append (RequestUtils.evaluateDecoderResult (בקשה)); אם (msg מופע של HttpContent) {HttpContent httpContent = (HttpContent) msg; responseData.append (RequestUtils.formatBody (httpContent)); responseData.append (RequestUtils.evaluateDecoderResult (בקשה)); if (msg instanceof LastHttpContent) {טריילר LastHttpContent = (LastHttpContent) msg; responseData.append (RequestUtils.prepareLastResponse (בקשה, טריילר)); writeResponse (ctx, trailer, responseData); }}
חלל פרטי writeResponse (ChannelHandlerContext ctx) {FullHttpResponse response = new DefaultFullHttpResponse (HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER); ctx.write (תגובה); }
FormatParams StringBuilder (בקשת HttpRequest) {StringBuilder responseData = StringBuilder חדש (); QueryStringDecoder queryStringDecoder = QueryStringDecoder חדש (request.uri ()); מַפָּה
פורמט StringBuilderBody (HttpContent httpContent) {StringBuilder responseData = StringBuilder חדש (); תוכן ByteBuf = httpContent.content (); אם (content.isReadable ()) {responseData.append (content.toString (CharsetUtil.UTF_8) .toUpperCase ()) .append ("\ r \ n"); } return responseData; }
StringBuilder prepareLastResponse (בקשת HttpRequest, טריילר LastHttpContent) {StringBuilder responseData = StringBuilder חדש (); responseData.append ("להתראות! \ r \ n"); אם (! trailer.trailingHeaders (). isEmpty ()) {responseData.append ("\ r \ n"); עבור (שם CharSequence: trailer.trailingHeaders (). שמות ()) {עבור (ערך CharSequence: trailer.trailingHeaders (). getAll (שם)) {responseData.append ("כותרת נגררת P.S.:"); responseData.append (name) .append ("=") .append (value) .append ("\ r \ n"); }} responseData.append ("\ r \ n"); } return responseData; }
3.3. כתיבת התגובה
חלל פרטי writeResponse (ChannelHandlerContext ctx, טריילר LastHttpContent, תגובה StringBuilder) {boolean keepAlive = HttpUtil.isKeepAlive (בקשה); FullHttpResponse httpResponse = DefaultFullHttpResponse חדש (HTTP_1_1, ((HttpObject) טריילר). DecoderResult (). IsSuccess ()? בסדר: BAD_REQUEST, Unpooled.copiedBuffer (responseData.toString (), CharsetUtil.UTF_8); httpResponse.headers (). set (HttpHeaderNames.CONTENT_TYPE, "text / plain; charset = UTF-8"); אם (keepAlive) {httpResponse.headers (). setInt (HttpHeaderNames.CONTENT_LENGTH, httpResponse.content (). readableBytes ()); httpResponse.headers (). set (HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } ctx.write (httpResponse); אם (! keepAlive) {ctx.writeAndFlush (Unpooled.EMPTY_BUFFER) .addListener (ChannelFutureListener.CLOSE); }}
4.1. קבל בקשה
תלתל //127.0.0.1:8080?param1=one
פרמטר: PARAM1 = ONE Good Bye!
4.2. בקשת POST
תלתל -ד "תוכן לדוגמא" -X POST //127.0.0.1:8080
תוכן לדוגמא להתראות!
5. מסקנה