本文目的
- 通過一個簡易的聊天室案例,講述Netty的基本使用。同時分享案例代碼。
- 項目中用到了log4j2,junit5,同時分享這些基礎(chǔ)組件的使用。
- 項目中用到了awt,屬于古董技術(shù),只是用來做界面。非重點不用關(guān)注。
環(huán)境說明
開發(fā)工具:idea2023,jdk:1.8,Maven:3.6.3
maven依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xxx</groupId>
<artifactId>xxx</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>xxx</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.21</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
<!-- log4j2-slf4j-適配器 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.20.0</version>
</dependency>
<!-- log4j2 日志核心 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.96.Final</version>
</dependency>
<!-- 單元測試,Junit5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
日志配置
src/main/resources/log4j2.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!-- log4j2配置文件 -->
<!-- monitorInterval="30" 自動加載配置文件的間隔時間,不低于10秒;生產(chǎn)環(huán)境中修改配置文件,是熱更新,無需重啟應(yīng)用
status="info" 日志框架本身的輸出日志級別,可以修改為info, -->
<Configuration status="warn" monitorInterval="30">
<!-- 集中配置屬性,使用時通過:${LOG_HOME} -->
<properties>
<!-- 當(dāng)前項目名稱,供下方引用 -->
<property name="PROJECT_NAME" value="tank-battle"/>
<!-- 默認(rèn)日志格式-包名自動縮減(同步異步通用) -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%-5level|%-5t|%logger{1.}: %msg%n"/>
<!-- 日志格式-打印代碼的精確位置信息,類,方法,行。(建議同步使用)。異步如果打印位置信息,會有嚴(yán)重性能問題 -->
<property name="LOG_PATTERN_ALL" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%-5level|%-5t|%location: %msg%n"/>
<!-- 日志主目錄。如果想把日志輸出到tomcat底下時使用。 -->
<property name="LOG_HOME">${web:rootDir}/WEB-INF/logs</property>
</properties>
<!-- 日志打印輸出方式 -->
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout charset="UTF-8" Pattern="${LOG_PATTERN}"/>
</Console>
<RollingFile name="FileLog" fileName="logs/${PROJECT_NAME}.log" filePattern="logs/${PROJECT_NAME}-%d_%i.log">
<PatternLayout charset="UTF-8" Pattern="${LOG_PATTERN}"/>
<Policies>
<!-- 每天生成一個,同時如果超過10MB還會再生成 -->
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="50 MB"/>
</Policies>
<DefaultRolloverStrategy max="99"/>
</RollingFile>
</Appenders>
<!-- 將代碼路徑與上面的日志打印關(guān)聯(lián)起來 -->
<Loggers>
<!-- 當(dāng)前項目日志 -->
<Logger name="com.sjj" level="INFO" additivity="false">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="FileLog"/>
</Logger>
<!-- 第三方依賴項目日志 -->
<logger name="org.springframework" level="info"/>
<logger name="org.jboss.netty" level="warn"/>
<!--日志級別以及優(yōu)先級排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!-- 根節(jié)點日志,除了上面配置的之外的日志 -->
<Root level="WARN">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="FileLog"/>
</Root>
</Loggers>
</Configuration>
單元測試
確認(rèn)項目已加入Junit5依賴,就是如下這段。
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
新建單元測試類的步驟。
- 在要創(chuàng)建單元測試的功能類上,依次點Code > generate > Test
- 然后在彈出的窗口中,選擇Junit版本為5,測試類名,測試方法等。然后點確定。
- IDEA會自動根據(jù)功能類的路徑在test目錄中創(chuàng)建相同路徑但以Test結(jié)尾的測試類。并且會自動生成勾選方法的默認(rèn)測試代碼。
- 根據(jù)程序的輸入和輸出,編寫單元測試代碼。
- 點擊方法左邊的綠色三角形就可以執(zhí)行單元測試用例了。
為什么要進(jìn)行單元測試?
- 方法內(nèi)部可以很復(fù)雜,如果靠肉眼觀察,比較耗時間。單元測試可以根據(jù)入?yún)⒑头祷刂禍y試方法是否達(dá)到要求。
- 代碼是開發(fā)人員寫的,最了解代碼邏輯的還是開發(fā)人員。測試人員測試不到代碼細(xì)節(jié)。
- 在一個大的功能中,可能會有很多方法,每個方法都要寫Main方法來一個個測試比較復(fù)雜,而且也不知道測了哪些場景。
為什么有的公司不做單元測試。
- 代碼業(yè)務(wù)可能比較簡單,程序員讀代碼不是很費(fèi)力。
- 寫單元測試需要額外花時間,程序員工作比較忙,沒時間寫。
功能介紹
簡易版聊天室程序。主要用于練習(xí)Netty的使用。聊天室功能如下:
- 聊天室支持多客戶端,每個客戶端都可以看到其他客戶端的消息。
- 點擊關(guān)閉按鈕時,關(guān)閉當(dāng)前客戶端,同時在服務(wù)端的客戶端列表中也刪除。
- 系統(tǒng)UI非重點,一切從簡。
開發(fā)步驟
-
首先寫一個聊天室的界面(ChatFrame.java)
-
參考坦克大戰(zhàn)的界面部分,設(shè)置好聊天室的長寬和坐標(biāo)。
-
界面包含2個輸入部分,中間文本域顯示當(dāng)前聊天室的所有聊天內(nèi)容。底部文本框輸入當(dāng)前用戶的聊天內(nèi)容
-
聊天室窗口初始化時,需要與服務(wù)端建立連接。
-
當(dāng)用戶輸入完聊天內(nèi)容后回車,需要將聊天內(nèi)容通過Netty客戶端發(fā)送給服務(wù)端。
-
當(dāng)用戶關(guān)閉窗口時,關(guān)閉當(dāng)前客戶端,同時在服務(wù)端的客戶端列表中也刪除。
-
/** * 聊天室客戶端-界面<br> * * @author namelessmyth * @version 1.0 * @date 2023/8/15 */ @Slf4j public class ChatFrame extends Frame { public static final int GAME_WIDTH = ConfigUtil.getInt("chat.frame.width"); public static final int GAME_HEIGHT = ConfigUtil.getInt("chat.frame.height"); TextArea ta = new TextArea(); TextField tf = new TextField(); public static final ChatFrame INSTANCE = new ChatFrame(); public static void main(String[] args) throws Exception { INSTANCE.setVisible(true); ChatClient.connect(); } private ChatFrame() throws HeadlessException { //創(chuàng)建游戲的主Frame this.setTitle("chat room"); this.setSize(GAME_WIDTH, GAME_HEIGHT); this.setLocation(800, 100); this.add(ta, BorderLayout.CENTER); this.add(tf, BorderLayout.SOUTH); tf.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { ChatClient.send(tf.getText()); tf.setText(""); } }); this.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { ChatClient.close(); System.exit(0); } }); log.info("chat room Main frame initialization completed"); } public void updateText(String text) { ta.setText(ta.getText() + Constants.LINE_SEPERATOR + text); } }
-
-
編寫Netty客戶端與服務(wù)端進(jìn)行消息通信(ChatClient.java)。
-
參考上面的描述,客戶端需要實現(xiàn)如下方法。
- connect(),與服務(wù)端建立連接的方法
- send(),向服務(wù)端發(fā)送聊天消息的方法。
- channelRead,讀取服務(wù)端信息更新客戶端聊天內(nèi)容方法
- 參考代碼如下
-
@Slf4j public class ChatClient { private static SocketChannel channel; /** * 與服務(wù)端建立連接的方法 */ public static void connect() { EventLoopGroup group = new NioEventLoopGroup(1); try { Bootstrap b = new Bootstrap(); b.group(group); b.channel(NioSocketChannel.class); b.handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { channel = ch; ch.pipeline().addLast(new MyClientHandler()); } }); ChannelFuture cf = b.connect("localhost", 8888).sync(); //直到服務(wù)器被關(guān)閉,否則一直阻塞。 cf.channel().closeFuture().sync(); log.info("the chat client has been closed."); } catch (Exception e) { log.error("ChatClient.connect.Exception.", e); } finally { group.shutdownGracefully(); } } /** * 向服務(wù)端發(fā)送聊天消息的方法 * @param msg 聊天內(nèi)容 */ public static void send(String msg) { channel.writeAndFlush(Unpooled.copiedBuffer(msg.getBytes())); log.info("client.send().{}", msg); } /** * 關(guān)閉客戶端方法,向服務(wù)端發(fā)送特定消息告知其刪除本客戶端。 */ public static void close() { send("__88__"); channel.close(); } } @Slf4j class MyClientHandler extends ChannelInboundHandlerAdapter { /** * 讀取服務(wù)端數(shù)據(jù) * @param msg 服務(wù)端數(shù)據(jù) */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; String text = buf.toString(StandardCharsets.UTF_8); ChatFrame.INSTANCE.updateText(text); log.info("channelRead.msg:{}", text); } /** * 連接剛建立時的事件處理 */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.info("connected to server."); } /** * 異常處理 */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.error("chat client exceptionCaught:", cause); super.exceptionCaught(ctx, cause); } }
-
-
聊天室服務(wù)端(ChatServer.java)。
-
服務(wù)端需要記錄所有的客戶端。(可能有多個)
-
當(dāng)某個客戶端發(fā)來消息之后,需要將消息轉(zhuǎn)發(fā)給所有客戶端。
-
當(dāng)接收到特殊消息時(客戶端關(guān)閉),需要將客戶端從列表中移除。
-
@Slf4j public class ChatServer { static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); public static void main(String[] args) throws Exception { //總管線程組 EventLoopGroup bossGroup = new NioEventLoopGroup(1); //接待員線程 EventLoopGroup workerGroup = new NioEventLoopGroup(2); //服務(wù)器啟動輔助類 ServerBootstrap b = new ServerBootstrap(); //放在第一位的是總管線程組,第二位的就是接待員線程組。 b.group(bossGroup, workerGroup); //異步全雙工 b.channel(NioServerSocketChannel.class); //接收到客戶端連接的處理,相當(dāng)于BIO的accept b.childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { log.info("a client connected:{}", sc); sc.pipeline().addLast(new MyChildHandler()); } }); b.bind(8888).sync(); } } @Slf4j class MyChildHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ChatServer.clients.add(ctx.channel()); } /** * 讀取客戶端通道內(nèi)的數(shù)據(jù) * @param msg 客戶端消息 * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; String str = buf.toString(StandardCharsets.UTF_8); log.info("channelRead().input,string:{},buf:{}", str, buf); if (StrUtil.equalsIgnoreCase(str, "__88__")) { ChatServer.clients.remove(ctx.channel()); ctx.close(); log.info("The chat client has been closed:{}", ctx.channel()); } else { ChatServer.clients.writeAndFlush(msg); log.info("ChatServer.clients.writeAndFlush:{}", msg); } } /** * 異常處理 * * @param ctx * @param cause * @throws Exception */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.error("exceptionCaught:", cause); ChatServer.clients.remove(ctx.channel()); ctx.close(); } }
-
補(bǔ)充服務(wù)端關(guān)閉的處理(僅思路,未實現(xiàn))。
- 通知客戶端,服務(wù)器準(zhǔn)備關(guān)閉。
- 拒絕新的連接接入
- 等待所有客戶端都處理完成。
- 開始關(guān)閉流程,發(fā)送消息給客戶端,客戶端自動處理。
- 確認(rèn)所有客戶端斷開。
- server保存現(xiàn)有的工作數(shù)據(jù)。
- 停止線程組
- 退出。
-
服務(wù)端UI
-
為了可以方便的看到所有客戶端的連接情況和消息,以及后續(xù)進(jìn)一步實現(xiàn)服務(wù)端的關(guān)閉效果考慮在服務(wù)端實現(xiàn)UI
-
新增一個ServerFrame類,實現(xiàn)服務(wù)端UI,服務(wù)端左邊顯示消息,右邊顯示客戶端的連接情況。
-
ServerFrame類初始化時自動啟動服務(wù)端。服務(wù)端接收消息時打印到消息窗口中。
-
有客戶端連上或者關(guān)閉時顯示到右邊的窗口中。
-
實現(xiàn)效果如下圖
-
-
參考代碼如下。(只需要修改服務(wù)端代碼,客戶端不變)
-
@Slf4j public class ServerFrame extends Frame { public static final int GAME_WIDTH = ConfigUtil.getInt("server.frame.width"); public static final int GAME_HEIGHT = ConfigUtil.getInt("server.frame.height"); TextArea tmsg = new TextArea("messages:"); TextArea tclient = new TextArea("clients:"); public static final ServerFrame INSTANCE = new ServerFrame(); public static void main(String[] args) throws Exception { INSTANCE.setVisible(true); ChatServer.start(); } private ServerFrame() throws HeadlessException { //創(chuàng)建游戲的主Frame this.setTitle("chat room"); this.setSize(GAME_WIDTH, GAME_HEIGHT); this.setLocation(100, 100); tmsg.setFont(new Font("Calibri",Font.PLAIN,20)); tclient.setFont(new Font("Calibri",Font.PLAIN,20)); Panel p = new Panel(new GridLayout(1, 2)); p.add(tmsg); p.add(tclient); this.add(p); this.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { System.exit(0); } }); log.info("Server Main frame initialization completed"); } public void updateMsg(String text) { tmsg.setText(tmsg.getText() + Constants.LINE_SEPERATOR + text); } public void updateClient(String text) { tclient.setText(tclient.getText() + Constants.LINE_SEPERATOR + text); } } @Slf4j public class ChatServer { static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); public static void start(){ //總管線程組 EventLoopGroup bossGroup = new NioEventLoopGroup(1); //接待員線程 EventLoopGroup workerGroup = new NioEventLoopGroup(2); try { //服務(wù)器啟動輔助類 ServerBootstrap b = new ServerBootstrap(); //放在第一位的是總管線程組,第二位的就是接待員線程組。 b.group(bossGroup, workerGroup); //異步全雙工 b.channel(NioServerSocketChannel.class); //接收到客戶端連接的處理,相當(dāng)于BIO的accept b.childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { log.info("a client connected:{}", sc); sc.pipeline().addLast(new MyChildHandler()); } }); log.info("chat server has been started"); ChannelFuture cf = b.bind(8888).sync(); cf.channel().closeFuture().sync(); } catch (Exception e) { log.error("ChatServer.exception", e); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); log.info("chat server has been closed"); } } } @Slf4j class MyChildHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ServerFrame.INSTANCE.updateClient("client connected:"+ctx.channel().remoteAddress()); ChatServer.clients.add(ctx.channel()); } /** * 讀取客戶端通道內(nèi)的數(shù)據(jù) * * @param msg 客戶端消息 * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; String str = buf.toString(StandardCharsets.UTF_8); log.info("channelRead().input,string:{},buf:{}", str, buf); if (StrUtil.equalsIgnoreCase(str, "__88__")) { ChatServer.clients.remove(ctx.channel()); ctx.close(); ServerFrame.INSTANCE.updateClient("client closed>"+ctx.channel().remoteAddress()); log.info("The chat client has been closed:{}", ctx.channel()); } else { ChatServer.clients.writeAndFlush(msg); ServerFrame.INSTANCE.updateMsg(ctx.channel().remoteAddress() + ">" + str); log.info("ChatServer.clients.writeAndFlush:{}", msg); } } /** * 異常處理 * * @param ctx * @param cause * @throws Exception */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.error("ChatServer.exceptionCaught:", cause); ChatServer.clients.remove(ctx.channel()); ctx.close(); } }
-
啟動順序。先啟動ServerFrame,然后啟動ChatFrame,ChatFrame可以啟動多個。
-
多個客戶端發(fā)送消息都會在服務(wù)端顯示。文章來源:http://www.zghlxwxcb.cn/news/detail-679666.html
-
-
參考說明
本文內(nèi)容主要來源于馬士兵老師的視頻教程(Java經(jīng)典實戰(zhàn)項目-坦克大戰(zhàn)),結(jié)合了老師的講課內(nèi)容以及自己的實踐做了一些補(bǔ)充。文章來源地址http://www.zghlxwxcb.cn/news/detail-679666.html
到了這里,關(guān)于Netty簡易聊天室的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!