介紹
Socket,ServerSocket
Socket就是我們所說的套接字,主要由IP地址和端口來表示,IP即目標服務器的IP地址。ServerSocket主要用在服務端,作用是監聽服務器的某一個端口。通過ServerSocket的accept()可以得到一個客戶端Socket,再通過輸入輸出流可以對其進行讀寫,從而實現服務器和客戶端之間的交互。
消息格式
既然這裏是需要實現私聊和羣聊,自然需要規定數據格式,這裏我採用JSON的格式來進行數據傳輸,這樣的目的是可以用JSON解析工具(如FastJSON)方便地對消息進行解析,降低代碼編寫難度提高效率。
其他的問題
1,因爲需要實現收發信息,而且收和發兩個動作是無序的,所以需要收和發兩個動作需要單獨的線程來進行單獨處理。
2,實現私聊,服務器需要根據用戶的唯一標識ID來轉發信息,所以需要對Socket再次封裝。
編寫
用戶對象
主要用來表示用戶的基本信息。
/**
* @author yintianhao
* @createTime 2020/6/30 0:16
* @description 用戶類
*/
public class User {
private String nickname;
private Integer id;
public User(String nickname, Integer id) {
this.nickname = nickname;
this.id = id;
}
public User(Integer id){
this.id = id;
}
//getter setter 略
}
消息對象
消息對象包括用戶發出的正文內容,消息種類(羣聊,私聊,初始化消息),消息來源(User對象),消息目的地(User對象)。通過對消息對象和JSON字符串相互轉化來實現消息的解析和生成。
/**
* @author yintianhao
* @createTime 2020/6/30 0:15
* @description 消息對象
*/
public class Msg {
//信息內容
private String content;
//信息種類,1羣聊,2私聊,3初始化消息
private Integer type;
private User from;
private User to;
public Msg(String content, Integer type, User from, User to){
this.content = content;
this.type = type;
this.from = from;
this.to = to;
}
//getter setter 略
}
服務端編寫
管道類
之前說過需要在Socket的基礎上進行再次封裝,封裝成管道類,管道用一個userId來唯一標識,也就是說一個管道可以看成一個用戶,在服務器啓動後,客戶端連接到服務器之後會發送一條初始化信息到服務端,從而在管道內的構造函數以內對初始化信息進行解析,整個管道的初始化到此完成。管道類另外一個作用就是實現讀寫分離。
/**
* @author yintianhao
* @createTime 2020/6/28 23:42
* @description 管道類,實現讀寫分離。
*/
public class Channel implements Runnable{
//log
private static Logger logger = Logger.getLogger(Channel.class);
//輸入輸出流
private DataOutputStream dataOutputStream;
private DataInputStream dataInputStream;
//客戶端套接字
private Socket client;
//運行標誌
private boolean isRunning;
//用戶列表
private CopyOnWriteArrayList<Channel> all;
private Integer userId;
public Channel(Socket client){
//初始化
logger.info("A client has connected");
this.client = client;
this.isRunning = true;
try {
this.dataInputStream = new DataInputStream(client.getInputStream());
this.dataOutputStream = new DataOutputStream(client.getOutputStream());
}catch (IOException e){
logger.info(e.getMessage());
//異常釋放資源
release();
}
//初始化管道id
String initMsg = receive();
logger.info("init message -- "+initMsg);
Msg msg = JSONUtil.getJsonObject(initMsg);
if (msg.getType() == 3) {
this.userId = Integer.parseInt(msg.getContent());
}
}
public void setChannelList(CopyOnWriteArrayList<Channel> all){
this.all = all;
}
//接收消息
private String receive(){
String msg = "";
try {
msg = dataInputStream.readUTF();
logger.info("received msg -- "+msg);
}catch (IOException e){
logger.info(e.getMessage());
//異常釋放資源
release();
}
return msg;
}
//發送單條信息
private void send(String content){
try {
logger.info("server transfer -- "+content);
dataOutputStream.writeUTF(content);
dataOutputStream.flush();
}catch (IOException e){
logger.info(e.getMessage());
//異常釋放資源
release();
}
}
//羣聊
private void sendOthers(String msg){
logger.info("Send msg to others");
logger.info("Channel list size -- "+all.size());
for (Channel c:all){
if(c!=this){
c.send(msg);
}
}
}
private void sendOne(String content){
logger.info("Send to someone");
//轉成json對象
Msg msg = JSON.parseObject(content,Msg.class);
User from = msg.getFrom();
User to = msg.getTo();
logger.info("Message from "+from.getId());
logger.info("Message to "+to.getId());
for (Channel c:all){
try {
if (c.userId.intValue()==to.getId().intValue()){
c.send(content);
break;
}
}catch (NullPointerException e){
logger.error(e.getMessage());
}
}
}
//釋放資源
public void release(){
//標誌改變
isRunning = false;
//關閉socket 輸入 輸出流
StreamUtil.close(dataInputStream,dataOutputStream,client);
logger.info("A client has released");
}
@Override
public void run() {
while(isRunning){
String content = receive();
Msg msg = JSON.parseObject(content,Msg.class);
//通過Msg的type來判斷是私聊還是羣聊
if (msg.getType()==1){
//羣聊
msg.setTo(null);
String publicMsg = JSONUtil.getJsonString(msg);
sendOthers(publicMsg);
logger.info("It is a public message");
}else if (msg.getType()==2){
//私聊
sendOne(content);
logger.info("It is a private message");
}else{
logger.info("It is an initial message");
}
}
}
}
啓動服務器
/**
* @author yintianhao
* @createTime 2020/6/29 1:16
* @description
*/
public class Server {
//logger
private static Logger logger = Logger.getLogger(Server.class);
public static void main(String[] args){
logger.info("Server has started");
try {
ServerSocket server = new ServerSocket(8888);
//創建容器
CopyOnWriteArrayList<Channel> channelList = new CopyOnWriteArrayList<>();
while(true){
//客戶端套接字
Socket client = server.accept();
//新建一個管道
Channel channel = new Channel(client);
//加入管道列表
channelList.add(channel);
//管道設置自己的管道列表
channel.setChannelList(channelList);
//開啓線程服務一個管道
Thread thread = new Thread(channel);
thread.start();
}
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
客戶端編寫
客戶端的管道有兩種,一種是發送管道,一種是接收管道。作用跟服務器的管道類似。由於這個demo是基於控制檯的,這裏用戶的ID和需要發送的消息類型都是根據控制檯輸入的。
發送管道
/**
* @author yintianhao
* @createTime 2020/6/29 1:31
* @description 發送
*/
public class SendChannel implements Runnable{
//控制檯輸入
private BufferedReader console;
//數據輸出流
private DataOutputStream dos;
//客戶端套接字
private Socket client;
//自身身份信息
private User user;
private Integer to;
private boolean isRunning;
private static Logger logger = Logger.getLogger(SendChannel.class);
public SendChannel(Socket client,User user,Integer to){
//初始化
console = new BufferedReader(new InputStreamReader(System.in));
this.client = client;
this.isRunning = true;
this.user = user;
this.to = to;
try {
dos = new DataOutputStream(client.getOutputStream());
} catch (IOException e) {
release();
logger.error(e.getMessage());
}
//發送id初始化管道
Msg initMsg = new Msg(String.valueOf(user.getId()),3,user,null);
send(JSONUtil.getJsonString(initMsg));
logger.info("SendChannel has inited");
}
public void release(){
isRunning = false;
StreamUtil.close(console,dos,client);
}
private String getMsgFromConsole(){
try {
String content = console.readLine();
//通過消息內容分割,strs[0]是消息類型
String[] strs = content.split("-");
Msg msg = new Msg(content,Integer.parseInt(strs[0]),user,new User(to));
return JSON.toJSONString(msg);
}catch (IOException e){
logger.error(e.getMessage());
}
return "";
}
//發送消息
private void send(String content){
try {
dos.writeUTF(content);
dos.flush();
}catch (IOException e){
logger.error(e.getMessage());
//異常釋放資源
release();
}
logger.info("Send -- "+content);
}
public User getUser(){
return user;
}
@Override
public void run() {
while(isRunning){
String msg = getMsgFromConsole();
if (!msg.equals("")){
send(msg);
}
}
}
}
接收管道
接收管道比較簡單,就是接收然後解析。
/**
* @author yintianhao
* @createTime 2020/6/29 1:31
* @description 接收
*/
public class ReceiveChannel implements Runnable {
private DataInputStream dos;
private Socket client;
private boolean isRunning;
private static Logger logger = Logger.getLogger(ReceiveChannel.class);
public ReceiveChannel(Socket client){
this.client = client;
isRunning = true;
try {
dos = new DataInputStream(client.getInputStream());
} catch (IOException e) {
logger.error(e.getMessage());
release();
}
logger.info("ReceiveChannel has inited");
}
private void release(){
isRunning = false;
StreamUtil.close(dos,client);
}
private String getMsgFromChannel(){
try {
String msg = dos.readUTF();
return msg;
} catch (IOException e) {
logger.error(e.getMessage());
release();
//e.printStackTrace();
}
return "";
}
@Override
public void run() {
while (isRunning){
String content = getMsgFromChannel();
Msg msg = JSON.parseObject(content,Msg.class);
if (msg!=null){
logger.info("Message from:"+msg.getFrom().getNickname()+"--"+msg.getContent());
}
}
}
}
啓動客戶端
由於這裏我沒有選擇從CMD輸入用戶暱稱,所以我用了三個Client類來模擬客戶端,另外兩個交Client1,Client2,三個類的區別只是User對象的暱稱不同。
/**
* @author yintianhao
* @createTime 2020/6/28 23:38
* @description 讀寫分離,封裝
*/
public class Client0 {
private static Logger logger = Logger.getLogger(Client0.class);
public static void main(String[] args){
logger.info("Client0 start,Input your id and other id");
try {
Scanner scanner = new Scanner(System.in);
String str = scanner.next();
int from = Integer.parseInt(str.split("-")[0]);
int to = Integer.parseInt(str.split("-")[1]);
//連接
Socket client = new Socket("127.0.0.1",8888);
//發送信息的線程
User user = new User("Yintianhao",from);
SendChannel sendChannel = new SendChannel(client,user,to);
Thread sendThread = new Thread(sendChannel);
sendThread.start();
ReceiveChannel receiveChannel = new ReceiveChannel(client);
Thread receiveThread = new Thread(receiveChannel);
receiveThread.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
演示
總結
其實這個demo雖然私聊羣聊是實現了,但是其實是存在許多不足的,比如消息的確認到達機制,消息丟失怎麼辦,消息的持久化,這些我都沒有考慮進去,這陣子我也還在學習這方面的內容,希望以這個demo爲開始繼續完善吧。(博客同步:https://izzer.cn/archives/20200707)