java入門(5)——通信初步(仿騰訊會議)
- 一、TCP/IP初步理論
- 二、接線員“Socket”——連接工具
- 三、IO流——傳輸工具
- 四、專案主體
- 客戶端
- UI界面
- NetConn接線員
- 監聽器(控制機制)
- Video執行緒
- 服務器
- 服務器執行緒
- 執行緒池
- 五、效果展示
一、TCP/IP初步理論
TCP/IP是指能夠在多個不同網路間實作資訊傳輸的協議簇,是網路中最基本的通信協議,其為各種資訊的傳輸制定了“標準”,
如果客戶端想要和服務器通信,一方面需要知道服務器映射出來的埠來保證“能連上”,另一方面需要熟悉服務器的接受協議保證“能收到”,
博主的“仿騰訊會議專案”便是建立在這一基礎之上實作的,
二、接線員“Socket”——連接工具
Socket也可以稱作“套接字”,是兩臺機器間通信的端點,
常用的構造方法:
Socket(InetAddress address, int port)
address便是你要連接的IP地址,對于本機連接(常用來做測驗)的話就是“127.0.0.1”,如果要想連接到其他小伙伴的路由器,需要知道他的對外的外網IP,
port是埠,連接IP地址的一個方式便是連接到該IP地址映射出來的埠,只有埠對應才能連接上,一般我們通過瀏覽器訪問的網址,默認的埠都是80或者8080,
Socket client=new Socket("127.0.0.1",9999);
System.out.println("連接成功!");
當然在客戶端連之前,需要保證服務器是開著的,通常用ServerSocket創建服務器,并在構造方法中進行埠映射,
//建立服務器
ServerSocket ss=new ServerSocket(9999);
System.out.println("服務器創建成功~!"+port);
那么我們可能會問,我們怎么才知道一個客戶端進來了呢?如果我們不知道客戶端有沒有進來,而是服務器胡亂沒有目的的發訊息,那必然會出錯,這里我們用到了阻塞IO流的方式,來等待客戶端進來,
Socket client=ss.accept();
System.out.println("有個客戶機進來了~!");
accept()方法,阻塞了IO流,在沒有客戶端進來的時候是不會執行下面的代碼,更不能呼叫服務器的IO流的,
當我們實作上述步驟后,便成功建立了客戶端和服務器的連接,
三、IO流——傳輸工具
接著,我們便要開始傳輸資訊了,其大致思想是獲取IO流,使用IO流進行傳輸,
//獲取Socket物件client的IO流
InputStream ins=client.getInputStream();
OutputStreamous=client.getOutputStream();
IO流就比較靈活了,但是通常我們使用資料流DataIO,因為DataIO比較靈活可以直接傳整型、字串等等,省去了用位元組流時編碼和解碼的兩步,
//將位元組流轉換成資料流使得我們用起來更靈活
dins=new DataInputStream(ins);
dous=new DataOutputStream(ous);
下面重點介紹報頭的重要性:
訊息傳輸時,實際上是先封包,以資料包的形式進行傳輸,資料包里裝的是編碼后的位元組表示,但是我們如何判斷發過來的是字串還是圖片還是其他的呢,所以這里就需要一個發送一個用來表征的東西,簡稱“報頭”
以發文本訊息(字串)為例:
public void SendText(String msg)
{
//以位元組的形式發送文本
try {
//報頭
dous.writeByte(3);
//長度
byte[] data=msg.getBytes();
int len=data.length;
dous.writeInt(len);
//發送字串內容
dous.write(data);
dous.flush();
System.out.println("客戶機發送字串len "+len+" msg "+msg);
}catch(Exception e)
{
e.printStackTrace();
System.out.println("******客戶機發送字串失敗");
}
}
dous.writeByte(3)發送報頭3,在客戶端和服務器規定好協議后,服務器在接受新的資料包時,收到3便知道收到的是文本訊息,便用文本訊息的讀取方式去讀取,以上便是報頭的思想,
四、專案主體
需求:
1、能夠傳畫出來的線條,
2、能夠傳實時的視頻圖象,
3、能夠傳文本訊息
客戶端
UI界面
①簡單地寫一個UI界面,有發送按鈕、選單欄、文本框、文本訊息顯示區域,
//子選單欄的建構式
public void addJmenuitem(JMenu jm,ClientListener cl,String str)
{
JMenuItem jmi=new JMenuItem(str);
jm.add(jmi);
jmi.addActionListener(cl);
}
public void initUI()
{
//界面大小和標題
this.setSize(900,1000);
this.setTitle("客戶端界面");
//創建監聽器
ClientListener cl=new ClientListener();
//選單欄
JMenuBar jmb=new JMenuBar();
this.add(jmb,BorderLayout.NORTH);
//創建選單
JMenu jm=new JMenu("功能");
jmb.add(jm);
//子選單
addJmenuitem(jm,cl,"線條傳輸");
addJmenuitem(jm,cl,"視頻傳輸");
addJmenuitem(jm,cl,"文本傳輸");
//發送按鈕,內容文本框,接受顯示多行文本框
JButton jbu=new JButton("Send");
this.add(jbu);
final JTextField jtf=new JTextField(40);
this.add(jtf);
JTextArea jta=new JTextArea(20,50);
this.add(jta);
this.setLayout(new FlowLayout());
this.setDefaultCloseOperation(3);
this.setVisible(true);
//畫筆
Graphics g=this.getGraphics();
//創建客戶機物件,也就是執行緒物件.創建視頻執行緒
final NetConn nc=new NetConn(g,jta);
Video vi=new Video();
//將客戶機物件和畫筆傳給監聽器
this.addMouseListener(cl);
cl.nc=nc;
cl.g=g;
cl.vi=vi;
//將客戶機物件、畫筆和flag傳給視頻執行緒
vi.g=g;
vi.nc=nc;
//客戶機連接
if(!nc.conn2Server())
return;
nc.start();
//匿名內部類加監聽器
jbu.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e)
{
String msg=jtf.getText();
nc.SendText(msg);
jtf.setText("");//清空文本框
}
});
}
public static void main(String args[])
{
ShaiZhaiQQ qqClient=new ShaiZhaiQQ();
qqClient.initUI();
}
}
NetConn接線員
②寫一個接線員NetConn類,用于創建客戶端、連接服務器、發送和接收訊息,注意由于,接收訊息是一直在進行的(只要服務器還活著),所以NetConn顯然本質上是一個執行緒,需要繼承Thread,并把接收訊息寫在run()方法里面,
public class NetConn extends Thread {
//指向界面上的多行文本框,發送時需要獲取其內容
private JTextArea jta;
//畫筆
private Graphics g;
//多行文本框構造器
public NetConn(Graphics g,JTextArea jta)
{
this.g=g;
this.jta=jta;
}
//雙向,輸入輸出資料流
private DataInputStream dins;
private DataOutputStream dous;
private ObjectOutputStream oos;
//發送線條
public void SendLine(int x1,int y1,int x2,int y2)
{
try {
//報頭
dous.writeInt(1);
//四個坐標
dous.writeInt(x1);
dous.writeInt(y1);
dous.writeInt(x2);
dous.writeInt(y2);
System.out.println("客戶機發送線條 ("+x1+","+y1+") -> ("+x2+","+y2+")");
}catch(Exception e)
{
e.printStackTrace();
System.out.println("******客戶機發送線條失敗");
}
}
//發送文本訊息給服務器
public void SendText(String msg)
{
//以位元組的形式發送文本
try {
//報頭
dous.writeByte(3);
//長度
byte[] data=msg.getBytes();
int len=data.length;
dous.writeInt(len);
//發送字串內容
dous.write(data);
dous.flush();
System.out.println("客戶機發送字串len "+len+" msg "+msg);
}catch(Exception e)
{
e.printStackTrace();
System.out.println("******客戶機發送字串失敗");
}
}
//發送和接收資訊
public void run()
{
try {
while(true)
{
//報頭
byte type=dins.readByte();
if(type==3)
{
//讀長度
int len=dins.readInt();
//讀字串內容并顯示在界面上,先讀位元組再轉字串
byte[] data=new byte[len];
dins.read(data);
String msg=new String(data);
this.jta.append(msg+"\r\n");
System.out.println("收到服務器發來的資訊"+msg);
}
}
}catch(Exception e)
{
e.printStackTrace();
System.out.println("*******客戶機讀訊息的執行緒出錯,退出~!");
}
}
//判斷是否連接成功
public boolean conn2Server()
{
try {
Socket client=new Socket("127.0.0.1",9999);
System.out.println("連接成功!");
//注意這個地方是先獲取輸入輸出流再利用構造器轉型成資料流
InputStream ins=client.getInputStream();
OutputStream ous=client.getOutputStream();
this.dins=new DataInputStream(ins);
this.dous=new DataOutputStream(ous);
this.oos=new ObjectOutputStream(ous);
return true;
}catch(Exception e)
{
e.printStackTrace();
System.out.println("******連接失敗~");
return false;
}
}
}
這里我把發送視頻的代碼單獨拿了出來,在起初我是用的是RGB色素點進行傳輸,也就是傳每一幀的二維RGB陣列,然而這樣傳輸非常非常非常慢!!!(測驗了一下,已經到了秒的級別)所以在某位鄒姓大佬的幫助下,我get到了用ImageIO的方式進行傳輸,基本上已經到了毫秒級別,這也說明了“大道至簡”:圖片直接編碼成位元組陣列,比其他花里胡哨的要快的多!!!
//發送視頻
public void SendVideo(BufferedImage image)
{
try {
//報頭
dous.writeInt(2);
//創建image的位元組陣列
byte[] imageData=null;
//位元組陣列輸出流
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ImageIO.write(image, "jpg", baos);
imageData=baos.toByteArray();
dous.writeInt(imageData.length);
dous.write(imageData);
}catch(Exception e)
{
e.printStackTrace();
System.out.println("******客戶機發送視頻失敗");
}
}
監聽器(控制機制)
這塊沒什么好說的,直接上原始碼了,
public class ClientListener implements MouseListener,ActionListener {
//flag表示三種功能
String flag;
//獲取兩個點的坐標
public int x1,x2,y1,y2;
//畫筆
public Graphics g;
NetConn nc;
Video vi;
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
flag=e.getActionCommand();
vi.flag=flag;
if(flag.equals("視頻傳輸"))
{vi.start();}
}
@Override
public void mouseClicked(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mousePressed(MouseEvent e) {
// TODO Auto-generated method stub
if(flag.equals("線條傳輸"))
{
x1=e.getX();
y1=e.getY();
}
}
@Override
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
if(flag.equals("線條傳輸"))
{
x2=e.getX();
y2=e.getY();
nc.SendLine(x1, y1, x2, y2);
g.drawLine(x1, y1, x2, y2);
}
}
@Override
public void mouseEntered(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseExited(MouseEvent e) {
// TODO Auto-generated method stub
}
}
Video執行緒
攝像頭的呼叫,使用了第三方庫Webcam,想解鎖更多Webcam功能的可以自行登錄官網連接(沒有打廣告!!!)
public class Video extends Thread {
//開關
String flag;
//畫筆
Graphics g;
//客戶端接線員
NetConn nc;
public void run()
{
while(true)
{
if(flag.equals("視頻傳輸"))
{
Webcam webcam=Webcam.getDefault();
webcam.open();
BufferedImage buffImage=webcam.getImage();
g.drawImage(buffImage, 0, 600, 300, 300,null);
//發送視頻
nc.SendVideo(buffImage);
}
try {
this.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
服務器
服務器基本上和客戶端沒有什么太大的差別,都需要一個基本的UI界面、接線員NetConn(因為你要獲取客戶端的輸入輸出流,實作雙向傳輸,可以理解成客戶端和服務器是共用輸入輸出流的),
區別的話,就是多了兩點:1、服務器執行緒DrawServer,2、執行緒池(客戶端集)
服務器執行緒
注意while(true)回圈讓服務器一直開著,可以接收多個客戶端,
public class DrawServer extends Thread {
//訊息顯示區域
private JTextArea jta;
//畫筆
public Graphics g;
public DrawServer(JTextArea jta,Graphics g)
{
this.jta=jta;
this.g=g;
}
public void run()
{
setServer(9999);
}
public void setServer(int port)
{
try {
//建立服務器
ServerSocket ss=new ServerSocket(9999);
System.out.println("服務器創建成功~!"+port);
//
while(true)
{
Socket client=ss.accept();
System.out.println("有個客戶機進來了~!");
NetConn nc=new NetConn(client,jta,g);
nc.start();
Tools.als.add(nc);
}
}catch(Exception e)
{
e.printStackTrace();
System.out.println("******服務器創建失敗");
}
}
}
執行緒池
tell the truth 其實就是一個佇列,用來存盤連進來的客戶端,這里有一個事情,望讀者注意,當客戶端退出時,一定要及時從佇列中刪去“死掉”客戶端,不然會導致記憶體泄漏甚至溢位,
public class Tools {
//私有構造保護,不要給別人創建這個類的物件的機會
private Tools()
{
}
//在服務器端,保存客戶機處理執行緒物件的佇列,也就是連接池
public static ArrayList<NetConn> als=new ArrayList();
//服務器廣播訊息,所有客戶端都能收到的訊息
public static void caseMsg(String msg)
{
for(int i=0;i<als.size();i++)
{
NetConn nc=als.get(i);
nc.SendText(msg);
}
}
}
五、效果展示

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/254921.html
標籤:java
上一篇:牛客編程題(十一)
