- 基于Netty網絡程式設計項目實戰課程
- 項目介紹
- Netty介紹與相關基礎知識
- Netty介紹
簡介
Netty是由JBOSS提供的一個java開源架構。Netty提供異步的、事件驅動的網絡應用程式架構和工具,用以快速開發高性能、高可靠性的網絡伺服器和用戶端程式。
也就是說,Netty 是一個基于NIO的客戶、伺服器端程式設計架構,使用Netty 可以確定你快速和簡單的開發出一個網絡應用,例如實作了某種協定的客戶、服務端應用。Netty相當于簡化和流線化了網絡應用的程式設計開發過程,例如:基于TCP和UDP的socket服務開發。
“快速”和“簡單”并不用産生維護性或性能上的問題。Netty 是一個吸收了多種協定(包括FTP、SMTP、HTTP等各種二進制文本協定)的實作經驗,并經過相當精心設計的項目。最終,Netty 成功的找到了一種方式,在保證易于開發的同時還保證了其應用的性能,穩定性和伸縮性。
- Netty提供了簡單易用的API
- 基于事件驅動的程式設計方式來編寫網絡通信程式
- 更高的吞吐量
- 學習難度低
應用場景:
JavaEE: Dubbo
大資料:Apache Storm(Supervisor worker程序間的通信也是基于Netty來實作的)
-
-
- BIO、NIO、AIO介紹與差別
-
阻塞與非阻塞
主要指的是通路IO的線程是否會阻塞(或者說是等待)
線程通路資源,該資源是否準備就緒的一種處理方式。
同步和異步
主要是指的資料的請求方式
同步和異步是指通路資料的一種機制
BIO
同步阻塞IO,Block IO,IO操作時會阻塞線程,并發處理能力低。
我們熟知的Socket程式設計就是BIO,一個socket連接配接一個處理線程(這個線程負責這個Socket連接配接的一系列資料傳輸操作)。阻塞的原因在于:作業系統允許的線程數量是有限的,多個socket申請與服務端建立連接配接時,服務端不能提供相應數量的處理線程,沒有配置設定到處理線程的連接配接就會阻塞等待或被拒絕。
NIO
同步非阻塞IO,None-Block IO
NIO是對BIO的改進,基于Reactor模型。我們知道,一個socket連接配接隻有在特點時候才會發生資料傳輸IO操作,大部分時間這個“資料通道”是空閑的,但還是占用着線程。NIO作出的改進就是“一個請求一個線程”,在連接配接到服務端的衆多socket中,隻有需要進行IO操作的才能擷取服務端的處理線程進行IO。這樣就不會因為線程不夠用而限制了socket的接入。
AIO(NIO 2.0)
異步非阻塞IO
這種IO模型是由作業系統先完成了用戶端請求處理再通知伺服器去啟動線程進行處理。AIO也稱NIO2.0,在JDK7開始支援。
-
-
- Netty Reactor模型 - 單線程模型、多線程模型、主從多線程模型介紹
- 單線程模型
- Netty Reactor模型 - 單線程模型、多線程模型、主從多線程模型介紹
-
使用者發起IO請求到Reactor線程
Ractor線程将使用者的IO請求放入到通道,然後再進行後續處理
處理完成後,Reactor線程重新獲得控制權,繼續其他用戶端的處理
這種模型一個時間點隻有一個任務在執行,這個任務執行完了,再去執行下一個任務。
- 但單線程的Reactor模型每一個使用者事件都在一個線程中執行:
- 性能有極限,不能處理成百上千的事件
- 當負荷達到一定程度時,性能将會下降
- 某一個事件處理器發生故障,不能繼續處理其他事件
-
-
-
- Reactor多線程模型
-
-
Reactor多線程模型是由一組NIO線程來處理IO操作(之前是單個線程),是以在請求處理上會比上一中模型效率更高,可以處理更多的用戶端請求。
這種模式使用多個線程執行多個任務,任務可以同時執行
但是如果并發仍然很大,Reactor仍然無法處理大量的用戶端請求
-
-
-
- Reactor主從多線程模型
-
-
這種線程模型是Netty推薦使用的線程模型
這種模型适用于高并發場景,一組線程池接收請求,一組線程池處理IO。
-
-
- Netty - 基于web socket簡單聊天DEMO實作
-
後端編寫
導入依賴
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<target>1.8</target>
<source>1.8</source>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.15.Final</version>
</dependency>
</dependencies>
編寫Netty Server
public class WebsocketServer {
public static void main(String[] args) throws InterruptedException {
// 初始化主線程池(boss線程池)
NioEventLoopGroup mainGroup = new NioEventLoopGroup();
// 初始化從線程池(worker線程池)
NioEventLoopGroup subGroup = new NioEventLoopGroup();
try {
// 建立伺服器啟動器
ServerBootstrap b = new ServerBootstrap();
// 指定使用主線程池和從線程池
b.group(mainGroup, subGroup)
// 指定使用Nio通道類型
.channel(NioServerSocketChannel.class)
// 指定通道初始化器加載通道處理器
.childHandler(new WsServerInitializer());
// 綁定端口号啟動伺服器,并等待伺服器啟動
// ChannelFuture是Netty的回調消息
ChannelFuture future = b.bind(9090).sync();
// 等待伺服器socket關閉
future.channel().closeFuture().sync();
} finally {
// 優雅關閉boos線程池和worker線程池
mainGroup.shutdownGracefully();
subGroup.shutdownGracefully();
}
}
}
編寫通道初始化器
public class WsServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// ------------------
// 用于支援Http協定
// websocket基于http協定,需要有http的編解碼器
pipeline.addLast(new HttpServerCodec());
// 對寫大資料流的支援
pipeline.addLast(new ChunkedWriteHandler());
// 添加對HTTP請求和響應的聚合器:隻要使用Netty進行Http程式設計都需要使用
// 對HttpMessage進行聚合,聚合成FullHttpRequest或者FullHttpResponse
// 在netty程式設計中都會使用到Handler
pipeline.addLast(new HttpObjectAggregator(1024 * 64));
// ---------支援Web Socket -----------------
// websocket伺服器處理的協定,用于指定給用戶端連接配接通路的路由: /ws
// 本handler會幫你處理一些握手動作: handshaking(close, ping, pong) ping + pong = 心跳
// 對于websocket來講,都是以frames進行傳輸的,不同的資料類型對應的frames也不同
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// 添加自定義的handler
pipeline.addLast(new ChatHandler());
}
}
編寫處理消息的ChannelHandler
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
// 用于記錄和管理所有用戶端的Channel
private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 擷取從用戶端傳輸過來的消息
String text = msg.text();
System.out.println("接收到的資料:" + text);
// 将接收到消息發送到所有用戶端
for(Channel channel : clients) {
// 注意所有的websocket資料都應該以TextWebSocketFrame進行封裝
channel.writeAndFlush(new TextWebSocketFrame("[伺服器接收到消息:]"
+ LocalDateTime.now() + ",消息為:" + text));
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 将channel添加到用戶端
clients.add(ctx.channel());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 當觸發handlerRemoved,ChannelGroup會自動移除對應用戶端的channel
//clients.remove(ctx.channel());
// asLongText()——唯一的ID
// asShortText()——短ID(有可能會重複)
System.out.println("用戶端斷開, channel對應的長id為:" + ctx.channel().id().asLongText());
System.out.println("用戶端斷開, channel對應的短id為:" + ctx.channel().id().asShortText());
}
}
-
-
- websocket以及前端代碼編寫
-
WebSocket protocol 是HTML5一種新的協定。它實作了浏覽器與伺服器全雙工通信(full-duplex)。一開始的握手需要借助HTTP請求完成。Websocket是應用層第七層上的一個應用層協定,它必須依賴 HTTP 協定進行一次握手,握手成功後,資料就直接從 TCP 通道傳輸,與 HTTP 無關了。
前端編寫
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div>發送消息</div>
<input type="text" id="msgContent" />
<input type="button" value="點選發送" οnclick="CHAT.chat()"/>
<div>接收消息:</div>
<div id="recMsg" style="background-color: gainsboro;"></div>
<script type="application/javascript">
window.CHAT = {
socket: null,
init: function() {
// 判斷浏覽器是否支援websocket
if(window.WebSocket) {
// 支援WebScoekt
// 連接配接建立socket,注意要添加ws字尾
CHAT.socket = new WebSocket("ws://127.0.0.1:9001/ws");
CHAT.socket.onopen = function() {
console.log("連接配接建立成功");
};
CHAT.socket.onclose = function() {
console.log("連接配接關閉")
};
CHAT.socket.onerror = function() {
console.log("發生錯誤");
};
CHAT.socket.onmessage = function(e) {
console.log("接收到消息:" + e.data);
var recMsg = document.getElementById("recMsg");
var html = recMsg.innerHTML;
recMsg.innerHTML = html + "<br/>" + e.data;
};
}
else {
alert("浏覽器不支援websocket協定");
}
},
chat: function() {
var msg = document.getElementById("msgContent");
CHAT.socket.send(msg.value);
}
}
CHAT.init();
</script>
</body>
</html>
-
-
- MUI、HTML5+、HBuilder介紹
-
MUI介紹
http://dev.dcloud.net.cn/mui/
MUI是一個輕量級的前端架構。MUI以iOS平台UI為基礎,補充部分Android平台特有的UI控件。MUI不依賴任何第三方JS庫,壓縮後的JS和CSS檔案僅有100+K和60+K,可以根據自己的需要,自定義去下載下傳對應的子產品。并且MUI編寫的前端,可以打包成APK和IPA安裝檔案,在手機端運作。也就是,編寫一套代碼,就可以在Android、IOS下運作。
API位址:http://dev.dcloud.net.cn/mui/ui/
H5+
H5+提供了對HTML5的增強,提供了40WAPI給程式員使用。使用H5+ API可以輕松開發二維碼掃描、攝像頭、地圖位置、消息推送等功能
API位址:http://www.html5plus.org/doc/zh_cn/accelerometer.html#
HBuilder
前端開發工具。本次項目所有的前端使用HBuilder開發。在項目開發完後,也會使用HBuilder來進行打包Android/IOS的安裝包。
http://www.dcloud.io/
-
-
- MUI前端開發
- 建立項目/頁面/添加MUI元素
- MUI前端開發
-
建立MUI移動App項目
頁面建立,添加元件
<header class="mui-bar mui-bar-nav">
<h1 class="mui-title">登入頁面</h1>
</header>
<div class="mui-content">
<form class="mui-input-group">
<div class="mui-input-row">
<label>使用者名</label>
<input type="text" class="mui-input-clear" placeholder="請輸入使用者名">
</div>
<div class="mui-input-row">
<label>密碼</label>
<input type="password" class="mui-input-password" placeholder="請輸入密碼">
</div>
<div class="mui-button-row">
<button type="button" class="mui-btn mui-btn-primary">确認</button>
<button type="button" class="mui-btn mui-btn-danger">取消</button>
</div>
</form>
</div>
http://dev.dcloud.net.cn/mui/ui/#accordion
-
-
-
- 擷取頁面元素/添加點選事件
-
-
擷取頁面元素
mui.plusReady(function() {
// 使用document.getElementById來擷取Input元件資料
var username = document.getElementById("username");
var password = document.getElementById("password");
var confirm = document.getElementById("confirm");
// 綁定事件
confirm.addEventListener("tap", function() {
alert("按下按鈕");
});
});
批量綁定頁面元素的點選事件
mui(".mui-table-view").on('tap','.mui-table-view-cell',function(){
});
使用原生JS的事件綁定方式
// 綁定事件
confirm.addEventListener("tap", function() {
alert("按下按鈕");
});
-
-
-
- 發起ajax請求
-
-
前端
當我們點選确認按鈕的時候,将使用者名和密碼發送給後端伺服器
// 發送ajax請求
mui.ajax('http://192.168.1.106:9000/login', {
data: {
username: username.value,
password: password.value
},
dataType: 'json', //伺服器傳回json格式資料
type: 'post', //HTTP請求類型
timeout: 10000, //逾時時間設定為10秒;
headers: {
'Content-Type': 'application/json'
},
success: function(data) {
// 可以使用console.log列印資料,一般用于調試
console.log(data);
},
error: function(xhr, type, errorThrown) {
//異常處理;
console.log(type);
}
});
後端
基于SpringBoot編寫一個web應用,主要是用于接收ajax請求,響應一些資料到前端
@RestController
public class LoginController {
@RequestMapping("/login")
public Map login(@RequestBody User user) {
System.out.println(user);
Map map = new HashMap<String, Object>();
if("tom".equals(user.getUsername()) && "123".equals(user.getPassword())) {
map.put("success", true);
map.put("message", "登入成功");
}
else {
map.put("success", false);
map.put("message", "登入失敗,請檢查使用者名和密碼是否輸入正确");
}
return map;
}
}
-
-
-
- 字元串轉JSON對象以及JSON對象轉字元串
-
-
将JSON對象轉換為字元串
// 使用JSON.stringify可以将JSON對象轉換為String字元串
console.log(JSON.stringify(data));
将字元串轉換為JSON對象
var jsonObj = JSON.parse(jsonStr);
-
-
-
- 頁面跳轉
-
-
mui.openWindow({
url: 'login_succss.html',
id:'login_succss.html'
});
-
-
-
- App用戶端緩存操作
-
-
大量的App很多時候都需要将伺服器端響應的資料緩存到手機App本地。
http://www.html5plus.org/doc/zh_cn/storage.html
在App中緩存的資料,就是以key-value鍵值對來存放的。
将資料放入到本地緩存中
var user = {
username: username.value,
password: password.value
}
// 将對象資料放入到緩存中,需要轉換為字元串
plus.storage.setItem("user", JSON.stringify(user));
從本地緩存中讀取資料
// 從storage本地緩存中擷取對應的資料
var userStr = plus.storage.getItem("user");
-
- 建構項目
- 項目功能需求、技術架構介紹
- 建構項目
功能需求
登入/注冊
個人資訊
搜尋添加好友
好友聊天
技術架構
前端
開發工具:HBuilder
架構:MUI、H5+
後端
開發工具:IDEA
架構:Spring Boot、MyBatis、Spring MVC、FastDFS、Netty
資料庫:mysql
-
-
- 使用模拟器進行測試
-
安裝附件中的夜神Android模拟器(nox_setup_v6.2.3.8_full.exe)
輕按兩下桌面圖示啟動模拟器
安裝後找到模拟器的安裝目錄
到指令行中執行以下指令
nox_adb connect 127.0.0.1:62001
nox_adb devices
進入到Hbuilder安裝目錄下的tools/adbs目錄
切換到指令行中執行以下指令
adb connect 127.0.0.1:62001
adb devices
打開HBuilder開始調試
-
-
- 前端 - HBuilder前端項目導入
-
将資料中的heima-chat.zip解壓,并導入到HBuilder中。
-
-
- 後端 - 導入資料庫/SpringBoot項目/MyBatis逆向工程
-
導入資料庫
将資料中的hchat.sql腳本在開發工具中執行
資料庫表結構介紹
tb_user使用者表
tb_friend朋友表
tb_friend_req申請好友表
tb_chat_record聊天記錄表
使用MyBatis逆向工程生成代碼
将資料中的generatorSqlmapCustom項目導入到IDEA中,并配置項目所使用的JDK
建立Spring Boot項目
拷貝資料pom.xml依賴
拷貝資料中的application.properties配置檔案
-
-
- 後端 - Spring Boot整合Netty搭建背景
-
spring boot整合Netty
導入資料中配置檔案中的spring-netty檔案夾中的java檔案
啟動Spring Boot,導入HTML頁面,使用浏覽器打開測試Netty是否整合成功
-
- 業務開發 - 使用者注冊/登入/個人資訊
-
-
- 使用者登入功能 -後端開發
-
導入IdWorker.java雪花算法ID生成器
初始化IdWorker
@SpringBootApplication
@MapperScan(basePackages = "com.itheima.hchat.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Bean
public IdWorker idWorker() {
return new IdWorker(0, 0);
}
}
建立Result實體類
public class Result {
private boolean success; // 是否操作成功
private String message; // 傳回消息
private Object result; // 傳回附件的對象
public Result(boolean success, String message) {
this.success = success;
this.message = message;
}
public Result(boolean success, String message, Object result) {
this.success = success;
this.message = message;
this.result = result;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
}
建立傳回給用戶端的User實體類
public class User {
private String id;
private String username;
private String picSmall;
private String picNormal;
private String nickname;
private String qrcode;
private String clientId;
private String sign;
private Date createtime;
private String phone;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPicSmall() {
return picSmall;
}
public void setPicSmall(String picSmall) {
this.picSmall = picSmall;
}
public String getPicNormal() {
return picNormal;
}
public void setPicNormal(String picNormal) {
this.picNormal = picNormal;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getQrcode() {
return qrcode;
}
public void setQrcode(String qrcode) {
this.qrcode = qrcode;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
public Date getCreatetime() {
return createtime;
}
public void setCreatetime(Date createtime) {
this.createtime = createtime;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", picSmall='" + picSmall + '\'' +
", picNormal='" + picNormal + '\'' +
'}';
}
}
UserController實作
@RequestMapping("/login")
public Result login(@RequestBody TbUser user) {
try {
User _user = userService.login(user.getUsername(), user.getPassword());
if(_user == null) {
return new Result(false, "登入失敗,将檢查使用者名或者密碼是否正确");
}
else {
return new Result(true, "登入成功", _user);
}
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "登入錯誤");
}
}
UserService接口定義
User login(TbUser user);
編寫UserServiceImpl實作
@Override
public User login(TbUser user) {
TbUserExample example = new TbUserExample();
TbUserExample.Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(user.getUsername());
List<TbUser> userList = userMapper.selectByExample(example);
if(userList != null && userList.size() == 1) {
TbUser userInDB = userList.get(0);
// MD5加密認證
if(userInDB.getPassword().equals(DigestUtils.md5DigestAsHex(user.getPassword().getBytes()))) {
return loadUserById(userInDB.getId());
}
else {
throw new RuntimeException("使用者名或密碼錯誤");
}
}
else {
throw new RuntimeException("使用者不存在");
}
}
-
-
- 使用者登入功能 - 前端&測試
- 注冊功能 - 後端
-
UserController
@RequestMapping("/register")
public Result register(@RequestBody TbUser user) {
try {
userService.register(user);
return new Result(true, "注冊成功");
} catch (RuntimeException e) {
return new Result(false, e.getMessage());
}
}
UserService接口
void register(TbUser user);
UserServiceImpl實作
@Override
public void register(TbUser user) {
// 1. 查詢使用者是否存在
TbUserExample example = new TbUserExample();
TbUserExample.Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(user.getUsername());
List<TbUser> userList = userMapper.selectByExample(example);
// 1.1 如果存在抛出異常
if(userList != null && userList.size() > 0 ) {
throw new RuntimeException("使用者名已經存在!");
}
else {
user.setId(idWorker.nextId());
// MD5加密儲存
user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes()));
user.setPicSmall("");
user.setPicNormal("");
user.setNickname(user.getUsername());
user.setQrcode("");
user.setCreatetime(new Date());
userMapper.insert(user);
}
}
-
-
- 注冊功能 - 前端&測試
- FASTDFS - 檔案伺服器介紹與搭建
-
什麼是FastDFS
FastDFS 是用 c 語言編寫的一款開源的分布式檔案系統。FastDFS 為網際網路量身定制,充分考慮了備援備份、負載均衡、線性擴容等機制,并注重高可用、高性能等名額,使用 FastDFS很容易搭建一套高性能的檔案伺服器叢集提供檔案上傳、下載下傳等服務。
FastDFS 架構包括 Tracker server 和 Storage server。用戶端請求 Tracker server 進行檔案上傳、下載下傳,通過 Tracker server 排程最終由 Storage server 完成檔案上傳和下載下傳。
Tracker server 作用是負載均衡和排程,通過 Tracker server 在檔案上傳時可以根據一些政策找到 Storage server 提供檔案上傳服務。可以将 tracker 稱為追蹤伺服器或排程伺服器。
Storage server 作用是檔案存儲,用戶端上傳的檔案最終存儲在 Storage 伺服器上,Storageserver 沒有實作自己的檔案系統而是利用作業系統 的檔案系統來管理檔案。可以将storage稱為存儲伺服器。
服務端兩個角色:
Tracker:管理叢集,tracker 也可以實作叢集。每個 tracker 節點地位平等。收集 Storage 叢集的狀态。
Storage:實際儲存檔案 Storage 分為多個組,每個組之間儲存的檔案是不同的。每個組内部可以有多個成員,組成員内部儲存的内容是一樣的,組成員的地位是一緻的,沒有主從的概念。
在Linux中搭建FastDFS
解壓縮fastdfs-image-server.zip
輕按兩下vmx檔案,然後啟動。
注意:遇到下列提示選擇“我已移動該虛拟機”!
IP位址已經固定為192.168.25.133 ,請設定你的僅主機網段為25。
登入名為root 密碼為itcast
-
-
- FASTDFS - 整合Spring Boot
-
導入ComponetImport.java工具類
導入FastDFSClient.java、FileUtils.java工具類
-
-
- 個人資訊 - 後端照片上傳功能開發
-
注入FastDFS相關Bean
@Autowired
private Environment env;
@Autowired
private FastDFSClient fastDFSClient;
編寫UserController update Handler上傳照片
@RequestMapping("/upload")
public Result upload(MultipartFile file, String userid) {
try {
// 上傳
String url = fastDFSClient.uploadFace(file);
String suffix = "_150x150.";
String[] pathList = url.split("\\.");
String thumpImgUrl = pathList[0] + suffix + pathList[1];
// 更新使用者頭像
User user = userService.updatePic(userid, url, thumpImgUrl);
user.setPicNormal(env.getProperty("fdfs.httpurl") + user.getPicNormal());
user.setPicSmall(env.getProperty("fdfs.httpurl") + user.getPicSmall());
return new Result(true, "上傳成功", user);
} catch (IOException e) {
e.printStackTrace();
return new Result(false, "上傳失敗");
}
}
編寫UserService
将新上傳的圖檔儲存到使用者資訊資料庫中
User updatePic(String userid, String url, String thumpImgUrl);
編寫UserServiceImpl
@Override
public User updatePic(String userid, String url, String thumpImgUrl) {
TbUser user = userMapper.selectByPrimaryKey(userid);
user.setPicNormal(url);
user.setPicSmall(thumpImgUrl);;
userMapper.updateByPrimaryKey(user);
User userVo = new User();
BeanUtils.copyProperties(user, userVo);
return userVo;
}
-
-
- 個人資訊 - 前端&測試頭像上傳
-
-
-
- 個人資訊 - 修改昵稱後端實作
-
編寫UserController
@RequestMapping("/updateNickname")
public Result updateNickname(@RequestBody TbUser user) {
try {
userService.updateNickname(user.getId(), user.getNickname());
return new Result(true, "修改成功");
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "修改失敗");
}
}
UserSevice接口
void updateNickname(String userid, String nickname);
UserServiceImpl實作
@Override
public void updateNickname(String userid, String nickname) {
System.out.println(userid);
TbUser user = userMapper.selectByPrimaryKey(userid);
user.setNickname(nickname);
userMapper.updateByPrimaryKey(user);
}
-
-
- 個人資訊 -重新加載使用者資訊後端實作
-
Controller
@RequestMapping("/findById")
public User findById(String userid) {
return userService.findById(userid);
}
UserService
User findById(String userid);
UserServiceImpl
@Override
public User findById(String userid) {
TbUser tbUser = userMapper.selectByPrimaryKey(userid);
User user = new User();
BeanUtils.copyProperties(tbUser, user);
return user;
}
-
-
- 個人資訊 - 修改昵稱前端測試
-
-
-
- 個人資訊 - 二維碼生成後端編寫
-
二維碼是在使用者注冊的時候,就根據使用者的使用者名來自動生成一個二維碼圖檔,并且儲存到FastDFS中。
需要對注冊的方法進行改造,在注冊使用者時,編寫邏輯儲存二維碼。并将二維碼圖檔的連結儲存到資料庫中。
二維碼前端頁面展示
導入二維碼生成工具類
導入QRCodeUtils.java檔案
UserServiceImpl
修改注冊方法,在注冊時,将使用二維碼生成工具将二維碼儲存到FastDFS,并儲存連結更新資料庫
@Override
public void register(TbUser user) {
// 1. 查詢使用者是否存在
TbUserExample example = new TbUserExample();
TbUserExample.Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(user.getUsername());
List<TbUser> userList = userMapper.selectByExample(example);
// 1.1 如果存在抛出異常
if(userList != null && userList.size() > 0 ) {
throw new RuntimeException("使用者名已經存在!");
}
else {
user.setId(idWorker.nextId());
// MD5加密儲存
user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes()));
user.setPicSmall("");
user.setPicNormal("");
user.setNickname(user.getUsername());
// 擷取臨時目錄
String tmpFolder = env.getProperty("hcat.tmpdir");
String qrCodeFile = tmpFolder + "/" + user.getUsername() + ".png";
qrCodeUtils.createQRCode(qrCodeFile, "user_code:" + user.getUsername());
try {
String url = fastDFSClient.uploadFile(new File(qrCodeFile));
user.setQrcode(url);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("上傳檔案失敗");
}
user.setCreatetime(new Date());
userMapper.insert(user);
}
}
-
-
- 個人資訊 - 二維碼生成前端測試
- 業務開發 - 發現頁面與通信錄
- 搜尋朋友 - 後端開發
-
在搜尋朋友的時候需要進行以下判斷:
- 不能添加自己為好友
- 如果搜尋的使用者已經是好友了,就不能再添加了
- 如果已經申請過好友并且好友并沒有處理這個請求了,也不能再申請。
前端頁面展示
搜尋朋友其實就是使用者搜尋,是以我們隻需要根據使用者名将對應的使用者搜尋出來即可。
編寫UserController
@RequestMapping("/findUserById")
public User findUserById(String userid) {
System.out.println(userid);
return userService.loadUserById(userid);
}
編寫UserService接口
User findUserById(String userid);
編寫UserServiceImpl實作
@Override
public User findUserById(String userid) {
TbUser tbUser = userMapper.selectByPrimaryKey(userid);
User user = new User();
BeanUtils.copyProperties(tbUser, user);
if(StringUtils.isNotBlank(user.getPicNormal())) {
user.setPicNormal(env.getProperty("fdfs.httpurl") + user.getPicNormal());
}
if(StringUtils.isNotBlank(user.getPicSmall())) {
user.setPicSmall(env.getProperty("fdfs.httpurl") + user.getPicSmall());
}
user.setQrcode(env.getProperty("fdfs.httpurl") + user.getQrcode());
return user;
}
-
-
- 搜尋朋友 - 前端測試聯調
- 添加好友 - 發送好友請求後端開發
-
添加好友需要發送一個好友請求。
編寫FriendController
@RequestMapping("/sendRequest")
public Result sendRequest(@RequestBody TbFriendReq tbFriendReq) {
try {
friendService.sendRequest(tbFriendReq);
return new Result(true, "發送請求成功");
}
catch (RuntimeException e) {
return new Result(false, e.getMessage());
}
catch (Exception e) {
e.printStackTrace();
return new Result(false, "發送請求失敗");
}
}
編寫FriendService
void sendRequest(TbFriendReq friendReq);
編寫FriendServiceImpl實作
@Override
public void sendRequest(TbFriendReq friendReq) {
// 判斷使用者是否已經發起過好友申請
TbFriendReqExample example = new TbFriendReqExample();
TbFriendReqExample.Criteria criteria = example.createCriteria();
criteria.andFromUseridEqualTo(friendReq.getFromUserid());
criteria.andToUseridEqualTo(friendReq.getToUserid());
List<TbFriendReq> friendReqList = friendReqMapper.selectByExample(example);
if(friendReqList == null || friendReqList.size() == 0) {
friendReq.setId(idWorker.nextId());
friendReq.setCreatetime(new Date());
// 設定請求未處理
friendReq.setStatus(0);
friendReqMapper.insert(friendReq);
}
else {
throw new RuntimeException("您已經請求過了");
}
}
-
-
- 添加好友 -前端測試
- 展示好友請求 -後端開發
-
前端頁面展示
編寫Controller
@RequestMapping("/findFriendReqByUserid")
public List<FriendReq> findMyFriendReq(String userid) {
return friendService.findMyFriendReq(userid);
}
編寫FriendService
List<FriendReq> findMyFriendReq(String userid);
編寫FriendServiceImpl實作
@Override
public List<FriendReq> findMyFriendReq(String userid) {
// 查詢好友請求
TbFriendReqExample example = new TbFriendReqExample();
TbFriendReqExample.Criteria criteria = example.createCriteria();
criteria.andToUseridEqualTo(userid);
// 查詢沒有處理的好友請求
criteria.andStatusEqualTo(0);
List<TbFriendReq> tbFriendReqList = friendReqMapper.selectByExample(example);
List<FriendReq> friendReqList = new ArrayList<FriendReq>();
// 加載好友資訊
for (TbFriendReq tbFriendReq : tbFriendReqList) {
TbUser tbUser = userMapper.selectByPrimaryKey(tbFriendReq.getFromUserid());
FriendReq friendReq = new FriendReq();
BeanUtils.copyProperties(tbUser, friendReq);
friendReq.setId(tbFriendReq.getId());
// 添加HTTP字首
friendReq.setPicSmall(env.getProperty("fdfs.httpurl") + friendReq.getPicSmall());
friendReq.setPicNormal(env.getProperty("fdfs.httpurl") + friendReq.getPicNormal());
friendReqList.add(friendReq);
}
return friendReqList;
}
-
-
- 展示好友請求 - 前端測試
- 添加好友 - 接受好友請求後端開發
-
添加好友需要雙方互相添加。
例如:A接受B的好友申請,則将A成為B的好友,同時B也成為A的好友。
編寫FriendController
@RequestMapping("/acceptFriendReq")
public Result acceptFriendReq(String reqid) {
try {
friendService.acceptFriendReq(reqid);
return new Result(true, "添加好友成功");
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "添加好友失敗");
}
}
編寫FriendService
void acceptFriendReq(String reqid);
編寫FriendServiceImpl
@Override
public void acceptFriendReq(String reqid) {
// 設定請求狀态為1
TbFriendReq tbFriendReq = friendReqMapper.selectByPrimaryKey(reqid);
tbFriendReq.setStatus(1);
friendReqMapper.updateByPrimaryKey(tbFriendReq);
// 互相添加為好友
// 添加申請方好友
TbFriend friend1 = new TbFriend();
friend1.setId(idWorker.nextId());
friend1.setUserid(tbFriendReq.getFromUserid());
friend1.setFriendsId(tbFriendReq.getToUserid());
friend1.setCreatetime(new Date());
// 添加接受方好友
TbFriend friend2 = new TbFriend();
friend2.setId(idWorker.nextId());
friend2.setFriendsId(tbFriendReq.getFromUserid());
friend2.setUserid(tbFriendReq.getToUserid());
friend2.setCreatetime(new Date());
friendMapper.insert(friend1);
friendMapper.insert(friend2);
// 發送消息更新通信錄
// 擷取發送好友請求方Channel
Channel channel = UserChannelMap.get(tbFriendReq.getFromUserid());
if(channel != null){
Message message = new Message();
message.setType(4);
channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message)));
}
}
-
-
- 添加好友 -拒絕添加好友後端開發
-
在使用者選擇忽略好友請求時,我們隻需要将之前的好友請求狀态(status)設定為1。無需添加好友。
編寫FriendController
@RequestMapping("/ignoreFriendReq")
public Result ignoreFriendReq(String reqid) {
try {
friendService.ignoreFriendReq(reqid);
return new Result(true, "忽略成功");
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "忽略失敗");
}
}
編寫FriendService接口
void ignoreFriendReq(String reqid);
編寫FriendServiceImpl實作
@Override
public void ignoreFriendReq(String reqId) {
// 設定請求狀态為1
TbFriendReq tbFriendReq = friendReqMapper.selectByPrimaryKey(reqId);
tbFriendReq.setStatus(1);
friendReqMapper.updateByPrimaryKey(tbFriendReq);
}
-
-
- 通信錄功能 - 後端
-
通信錄功能就是要根據目前登入使用者的id,擷取到使用者的好友清單。
前端頁面效果
編寫FriendController
@RequestMapping("/findFriendsByUserid")
public List<User> findFriendsByUserid(String userid) {
return friendService.findFriendsByUserid(userid);
}
編寫FriendService
List<User> findFriendsByUserid(String userid);
編寫FriendServiceImpl
@Override
public List<User> findFriendsByUserid(String userid) {
TbFriendExample example = new TbFriendExample();
TbFriendExample.Criteria criteria = example.createCriteria();
criteria.andUseridEqualTo(userid);
List<TbFriend> tbFriendList = friendMapper.selectByExample(example);
List<User> userList = new ArrayList<User>();
for (TbFriend tbFriend : tbFriendList) {
TbUser tbUser = userMapper.selectByPrimaryKey(tbFriend.getFriendsId());
User user = new User();
BeanUtils.copyProperties(tbUser, user);
// 添加HTTP字首
user.setPicSmall(env.getProperty("fdfs.httpurl") + user.getPicSmall());
user.setPicNormal(env.getProperty("fdfs.httpurl") + user.getPicNormal());
userList.add(user);
}
return userList;
}
-
- 業務開發 - 聊天業務
- 聊天業務 - 使用者id關聯Netty通道後端開發
- 業務開發 - 聊天業務
要使用netty來進行兩個用戶端之間的通信,需要提前建立好使用者id與Netty通道的關聯。
伺服器端需要對消息進行儲存。
每一個App用戶端登入的時候,就需要建立使用者id與通道的關聯。
導入SpringUtil工具類
此工具類主要用來在普通Java類中擷取Spring容器中的bean
定義消息實體類
public class Message implements Serializable{
private Integer type; // 消息類型
private TbChatRecord chatRecord; // 消息體
private String ext; // 擴充字段
// getter/setter
}
定義UserChannelMap用來儲存使用者id與Channel通道關聯
public class UserChannelMap {
public static HashMap<String, Channel> userChannelMap = new HashMap<>();
public static void put(String userid, Channel channel) {
userChannelMap.put(userid, channel);
}
public static Channel get(String userid) {
return userChannelMap.get(userid);
}
}
編寫ChatHandller
使用者在第一次登陸到手機App時,會自動發送一個type為0的消息,此時,需要建立使用者與Channel通道的關聯。後續,将會根據userid擷取到Channel,給使用者推送消息。
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
// 用于記錄和管理所有用戶端的Channel
private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 1. 擷取從用戶端傳輸過來的消息
String text = msg.text();
// 2. 判斷消息的類型,根據不同的消息類型執行不同的處理
System.out.println(text);
Message message = JSON.parseObject(text, Message.class);
Integer type = message.getType();
switch (type) {
case 0:
// 2.1 當websocket第一次Open的時候,初始化channel,channel關聯到userid
String userid = message.getChatRecord().getUserid();
// 儲存userid對應的channel
UserChannelMap.put(userid, channel);
for (Channel client : clients) {
System.out.println("用戶端連接配接id:" + client.id());
}
// 列印目前線上使用者
for(String uid : UserChannelMap.userChannelMap.keySet()) {
System.out.print("使用者id:" + uid + "\n\n");
System.out.println("Channelid:" + UserChannelMap.get(uid));
}
break;
case 1:
// 2.2 聊天記錄儲存到資料庫,标記消息的簽收狀态[未簽收]
break;
case 2:
// 2.3 簽收消息,修改資料庫中的消息簽收狀态[已簽收]
// 表示消息id的清單
break;
case 3:
// 2.4 心跳類型的消息
break;
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 将channel添加到用戶端
clients.add(ctx.channel());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 當觸發handlerRemoved,ChannelGroup會自動移除對應用戶端的channel
clients.remove(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 抛出異常時移除通道
cause.printStackTrace();
ctx.channel().close();
clients.remove(ctx.channel());
}
}
-
-
- 聊天業務 - 使用者斷開連接配接、連接配接異常取消關聯通道
-
伺服器端應該根據通道的ID,來取消使用者id與通道的關聯關系。
UserChannelMap類
public static void removeByChannelId(String channelId) {
if(!StringUtils.isNotBlank(channelId)) {
return;
}
for (String s : userChannelMap.keySet()) {
Channel channel = userChannelMap.get(s);
if(channelId.equals(channel.id().asLongText())) {
System.out.println("用戶端連接配接斷開,取消使用者" + s + "與通道" + channelId + "的關聯");
userChannelMap.remove(s);
break;
}
}
}
ChatHandler類
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
UserChannelMap.removeByChannelId(ctx.channel().id().asLongText());
ctx.channel().close();
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("關閉通道");
UserChannelMap.removeByChannelId(ctx.channel().id().asLongText());
UserChannelMap.print();
}
-
-
- 聊天業務 - 發送聊天消息後端開發
-
将消息發送到好友對應的Channel通道,并将消息記錄儲存到資料庫中
編寫ChatHandler
擷取ChatRecordService服務
Channel channel = ctx.channel();
ChatRecordService chatRecordService = (ChatRecordService) SpringUtil.getBean("chatRecordServiceImpl");
case 1:
// 2.2 聊天記錄儲存到資料庫,标記消息的簽收狀态[未簽收]
TbChatRecord chatRecord = message.getChatRecord();
String msgText = chatRecord.getMessage();
String friendid = chatRecord.getFriendid();
String userid1 = chatRecord.getUserid();
// 儲存到資料庫,并标記為未簽收
String messageId = chatRecordService.insert(chatRecord);
chatRecord.setId(messageId);
// 發送消息
Channel channel1 = UserChannelMap.get(friendid);
if(channel1 != null) {
// 從ChannelGroup查找對應的額Channel是否存在
Channel channel2 = clients.find(channel1.id());
if(channel2 != null) {
// 使用者線上,發送消息到對應的通道
System.out.println("發送消息到" + JSON.toJSONString(message));
channel2.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message)));
}
}
break;
編寫ChatRecordService接口
String insert(TbChatRecord chatRecord);
編寫ChatRecordServiceImpl實作
@Override
public String insert(TbChatRecord chatRecord) {
chatRecord.setId(idWorker.nextId());
chatRecord.setHasRead(0);
chatRecord.setCreatetime(new Date());
chatRecord.setHasDelete(0);
chatRecordMapper.insert(chatRecord);
return chatRecord.getId();
}
-
-
- 聊天業務 - 加載聊天記錄功能
-
根據userid和friendid加載未讀的聊天記錄
編寫ChatRecordController
@RequestMapping("/findUnreadByUserIdAndFriendId")
public List<TbChatRecord> findUnreadByUserIdAndFriendId(String userid, String friendid) {
return chatRecordService.findUnreadByUserIdAndFriendId(userid, friendid);
}
編寫ChatRecordService
List<TbChatRecord> findUnreadByUserIdAndFriendId(String userid, String friendId);
編寫ChatRecordServiceImpl實作
@Override
public List<TbChatRecord> findUnreadByUserIdAndFriendId(String userid, String friendid) {
TbChatRecordExample example = new TbChatRecordExample();
TbChatRecordExample.Criteria criteria1 = example.createCriteria();
criteria1.andUseridEqualTo(friendid);
criteria1.andFriendidEqualTo(userid);
criteria1.andHasReadEqualTo(0);
criteria1.andHasDeleteEqualTo(0);
TbChatRecordExample.Criteria criteria2 = example.createCriteria();
criteria2.andUseridEqualTo(userid);
criteria2.andFriendidEqualTo(friendid);
criteria2.andHasReadEqualTo(0);
criteria2.andHasDeleteEqualTo(0);
example.or(criteria1);
example.or(criteria2);
// 加載未讀消息
List<TbChatRecord> chatRecordList = chatRecordMapper.selectByExample(example);
// 将消息标記為已讀
for (TbChatRecord tbChatRecord : chatRecordList) {
tbChatRecord.setHasRead(1);
chatRecordMapper.updateByPrimaryKey(tbChatRecord);
}
return chatRecordList;
}
-
-
- 聊天業務 - 已讀/未讀消息狀态标記
-
已讀消息
當使用者接收到聊天消息,且聊天視窗被打開,就會發送一條用來簽收的消息到Netty伺服器
使用者打開聊天視窗,加載所有聊天記錄,此時會把發給他的所有消息設定為已讀
未讀消息
如果使用者沒有打開聊天視窗,就認為消息是未讀的
ChatRecordController
@RequestMapping("/findUnreadByUserid")
public List<TbChatRecord> findUnreadByUserid(String userid) {
try {
return chatRecordService.findUnreadByUserid(userid);
} catch (Exception e) {
e.printStackTrace();
return new ArrayList<TbChatRecord>();
}
}
ChatRecordService
void updateStatusHasRead(String id);
ChatRecordServiceImpl
@Override
public void updateStatusHasRead(String id) {
TbChatRecord tbChatRecord = chatRecordMapper.selectByPrimaryKey(id);
tbChatRecord.setHasRead(1);
chatRecordMapper.updateByPrimaryKey(tbChatRecord);
}
ChatHandler
case 2:
// 将消息記錄設定為已讀
chatRecordService.updateStatusHasRead(message.getChatRecord().getId());
break;
-
-
- 聊天業務 - 未讀消息讀取
-
在使用者第一次打開App的時候,需要将所有的未讀消息加載到App
ChatRecordController
@RequestMapping("/findUnreadByUserid")
public List<TbChatRecord> findUnreadByUserid(String userid) {
try {
return chatRecordService.findUnreadByUserid(userid);
} catch (Exception e) {
e.printStackTrace();
return new ArrayList<TbChatRecord>();
}
}
ChatRecordService
List<TbChatRecord> findUnreadByUserid(String userid);
ChatRecordServiceImpl
@Override
public List<TbChatRecord> findUnreadByUserid(String userid) {
TbChatRecordExample example = new TbChatRecordExample();
TbChatRecordExample.Criteria criteria = example.createCriteria();
// 設定查詢發給userid的消息
criteria.andFriendidEqualTo(userid);
criteria.andHasReadEqualTo(0);
return chatRecordMapper.selectByExample(example);
}
-
- 業務開發 - 心跳機制
- Netty心跳處理以及讀寫逾時設定
- 業務開發 - 心跳機制
Netty并不能監聽到用戶端設定為飛行模式時,自動關閉對應的通道資源。我們需要讓Netty能夠定期檢測某個通道是否空閑,如果空閑超過一定的時間,就可以将對應用戶端的通道資源關閉。
編寫後端Netty心跳檢查的Handler
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
// 用戶端在一定的時間沒有動作就會觸發這個事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 用于觸發使用者事件,包含讀空閑/寫空閑
if(evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent)evt;
if(event.state() == IdleState.READER_IDLE) {
System.out.println("讀空閑...");
}
else if(event.state() == IdleState.WRITER_IDLE) {
System.out.println("寫空閑...");
}
else if(event.state() == IdleState.ALL_IDLE) {
System.out.println("關閉用戶端通道");
// 關閉通道,避免資源浪費
ctx.channel().close();
}
}
}
}
在通道初始化器中(WebSocketInitailizer)添加心跳檢查
// 增加心跳事件支援
// 第一個參數: 讀空閑4秒
// 第二個參數: 寫空閑8秒
// 第三個參數: 讀寫空閑12秒
pipeline.addLast(new IdleStateHandler(4, 8, 12));
pipeline.addLast(new HeartBeatHandler());
-
-
- 測試Netty心跳機制
-