第11次文章:网络编程——聊天室构建
- 2019 年 10 月 8 日
- 筆記
这周的内容还是蛮有意思的!构建一个聊天室,如果我们20年前掌握了这篇文章的内容,那我们就离马化腾不远了!哈哈哈!
1、网络:将不同区域的计算机连接起来,比如:局域网、城域网、互联网。
2、地址:IP地址 确定网络上的一个绝对地址类似于:房子。
3、端口号:区分计算机不同软件,类似于房子的房门。端口号长度为2个字节,范围为:0—65535,共有65536个。
tips:在使用户端口号的时候,在同一个协议下,端口号不能重复,如果在不同协议下,则端口号可以重复。1024以下的端口号不要使用,主要是留给设备服务商使用的固定端口号
4、资源定位:
URL:统一资源定位符
URI:统一资源
5、数据的传输:
TCP协议:类似于打电话,有三次握手机制,面向连接,安全可靠,效率低下。
UDP协议:类似于发短信,非面向连接,效率高,但是不可靠,可能存在信息丢失的情况。
二、网络编程中的一些基本类
1、地址及端口:
(1)InetAddress:封装计算机的ip地址和DNS,没有端口
方法:
getLocalHost():获取本地地址
getHostName():返回域名
getHostAddress():返回IP地址
getByName():通过域名或者IP来获取地址
(2)InetSocketAddress:在InetAddress基础上+端口
创建对象:
InetSocketAddress(String hostname, int port)
InetSocketAddress(InetAddress addr, int port)
方法:
getHostName():获取域名
getPort():获取端口号
getAddress():获取InetAddress对象
2、URL:
四部分组成: 协议 存放资源的主机域名 端口 资源文件名(/)
(1)创建
URL(String spec):绝对路径构建
URL(URL context, String spec):相对路径构建
(2)方法
package com.peng.net.url; import java.net.MalformedURLException; import java.net.URL; public class URLDemo01 { public static void main(String[] args) throws MalformedURLException { //绝对路径构建 URL url = new URL("http://www.baidu.com:80/index.html#aa?uname=peng"); System.out.println("协议:"+url.getProtocol()); System.out.println("域名:"+url.getHost()); System.out.println("端口:"+url.getPort()); System.out.println("资源:"+url.getFile()); System.out.println("相对路径:"+url.getPath()); System.out.println("锚点:"+url.getRef()); System.out.println("参数:"+url.getQuery());//如果存在锚点,则将参数视为锚点的一部分,返回null;如果不存在锚点,则返回参数 //相对路径 url = new URL("http://www.baidu.com/a/"); url = new URL(url,"b/c.txt"); System.out.println(url.toString()); } }
三、UDP编程,基本概念:
UDP:以数据为中心,非面向连接,不安全,数据可能丢失,效率高。
1、客户端
1)创建客户端 DatagramSocket 类 +指定发送端口
2)编辑数据 字节数组
3)打包 DatagramPacket + 服务器地址 + 指定的接收端口
4)发送数据
5)释放资源
2、服务器端
1)创建服务器端 DatagramSocket 类 + 指定接收端口
2)创建接收容器 字节数组
3)打包封装 DatagramPacket
4)包 接收数据
5)分析
6)释放资源
由于UDP协议编程是非面向连接的,TCP协议编程面向连接,相比之下TCP更加复杂,所以此处不放入UDP编程进行讲解,我们结合后面的TCP编程进行解析UDP编程细节。
四、基于TCP编程:
面向连接 安全可靠 效率低,类似于打电话
1、面向连接:请求-响应 Request–Response
2、Socket编程
1)、服务器:SeverSocket
2)、客户端:Socket
基本的TCP相关协议我们在下面一个实例中进行讲解——聊天室创建,其中包含有群聊和私聊功能。
基本的通讯思路如下图所示:

在客户端首先和服务器端建立连接通道,也就是socket,然后在传输通道中进行数据的传输,每一个通道内的蓝色箭头,代表着数据的输入和输出流。并且在数据的发送和接收过程中,可以同时进行,不会受到彼此的影响。在同一个聊天室中,具有多个客户端,他们需要同时连接在我们的服务器端上,因此我们在设计的过程中需要进行多线程的应用。
第一步:我们首先对客户端的接收数据进行封装,创建接收通道。
package com.peng.net.tcp.chat.demo04; /** * 从服务器接收数据 */ import java.io.DataInputStream; import java.io.IOException; import java.net.Socket; public class Receive implements Runnable{ //管道输入流 private DataInputStream dis ; //线程标识符 private boolean isRunning = true; //构造器 public Receive() { } public Receive(Socket client) { try { //获取客户端与服务器之间的传输管道 dis = new DataInputStream(client.getInputStream()); } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dis); } } /** * 获取从服务器发送到客户端的数据 * @return */ public String receive() { String msg = ""; try { msg = dis.readUTF();//从管道输入流中读取发送过来的数据内容 } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dis); } return msg; } @Override public void run() { //线程体 while(isRunning) { System.out.println(receive());//在客户端的控制台上打印接收到的数据内容 } } }
解析:在接收数据的过程中,我们主要思路是,在构造器中对输入流进行初始化操作,应用“DataInputStream”输入流,然后加入一个接收方法,将管道中服务器传回来的数据进行读取,最后在线程体中,将读取到的内容传输到客户端的界面上。
由于我们在多线程的使用中,频繁使用关闭输入输出流的关闭操作,所以我们将输入输出流的关闭操作封装成为一个单独的类,这样便于我们后期的调用和处理。
package com.peng.net.tcp.chat.demo04; import java.io.Closeable; import java.io.IOException; /** * 关闭流 */ public class CloseUtil { public static void closeAll(Closeable... io) { for (Closeable tem:io) { try { if(null !=tem ) { tem.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
tips:在对其进行封装的过程中,注意我们使用到了代码“Closeable…”,其中的运算符“…”相当于数组“[]”。
第二步:我们对客户端的发送操作进行一个封装操作。
package com.peng.net.tcp.chat.demo04; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; /** * 发送数据线程 */ public class Send implements Runnable{ //控制台输入流 private BufferedReader console ; //管道输出流 private DataOutputStream dos ; //客户端的名称 private String name; //线程标识符 private boolean isRunning = true; //构造器:初始化输入输出流 public Send() { console = new BufferedReader(new InputStreamReader(System.in)); } public Send(Socket client,String name) { this(); try { //初始化客户端向服务器端发送信息的管道 dos = new DataOutputStream(client.getOutputStream()); this.name = name; this.send(this.name);//在接收到名称时,就将客户端的名称发送出去 } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dos,console); } } /** * 从控制台获取数据 * @return */ public String getMsgFromConsole() { try { return console.readLine();//获取控制台输入的数据 } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dos,console); } return ""; } /** * 将客户端的数据发送给服务器 * @param info */ public void send(String info) { try { if(null != info && !info.equals("")) { dos.writeUTF(info);//写出数据信息 dos.flush();//强制刷新 } } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dos,console); } } @Override public void run() { //线程体 while(isRunning) { send(getMsgFromConsole()); } } }
解析:在进行发送操作的时候,我们需要有两个流操作,一个是输入流,主要负责从控制台上接收客户端输入的数据,另一个是输出流,主要负责将从客户端上获取到的信息发送到服务器进行操作。所以我们为了降低方法之间的耦合性,使用了两个方法,分别封装其功能。在最后的线程体中,我们将接收到的数据直接发送给客户端。注意,我们在构造器中发送了一个名称给客户端,这一点在我们创建客户端的代码中会进行解释。
第三步:创建客户端
package com.peng.net.tcp.chat.demo04; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; /** * 创建客户端:发送数据+接收数据 * 写出数据:输出流 * 读取数据:输入流 * 同时需要将输入流和输出流分别封装起来,彼此独立,相互独立处理 * 加入客户端名称 */ public class Client { public static void main(String[] args) throws IOException { System.out.println("请输入名称:"); //从控制台输入客户端名称 BufferedReader br = new BufferedReader(new InputStreamReader(System.in));//新建输入流 String name = br.readLine();//获取客户端名称 if("" == name) {//如果名字为空,则退出 return; } //创建客户端,与服务器进行连接,并指定端口号 Socket client = new Socket("localhost",9999); new Thread(new Send(client,name)).start();//发送路径 new Thread(new Receive(client)).start();//接受路径 } }
解析:在创建客户端的时候,我们首先需要获取每一个客户端的名称,在获取到名称之后,我们立刻将客户端的名称发送给服务器后,服务器会进行一定的反馈,返回给客户端的消息为:“欢迎加入聊天室”,然后在其他客户端的界面上,输出“XXX加入了聊天室”。
tips:在UDP协议中,客户端发送数据的时候,需要指定客户端发送端口,以及服务器的接收端口,这一点与TCP协议编程中有所不同。在TCP编程中,客户端不需要指定对应的发送端口,系统会自动分配给客户端端口,但是并非TCP不要需要端口,只是开发者在编程的时候可以省略而已。
第四步:创建服务器
package com.peng.net.tcp.chat.demo04; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List; /** * 创建服务器 加入多线程 * 实现多个不同的客户端可以同时进行发送接收数据 */ public class Server { //存储所有客户端与服务器建立的管道 private List<MyChannel> all = new ArrayList<MyChannel>(); public static void main(String[] args) throws IOException { new Server().start(); } public void start() throws IOException { //创建服务器,指定端口号 ServerSocket server = new ServerSocket(9999); while(true) { Socket client = server.accept();//接收客户端请求,并与客户端建立连接 MyChannel channel = new MyChannel(client); all.add(channel);//向容器中加入客户端通道,便于统一管理 new Thread(channel).start();//一条道路 } } /** * 定义匿名内部类,便于调用类的属性,此匿名类相当于客户端和服务器之间建立的道路 * 一个客户,一条道路 * 1、输入流:接收数据 * 2、输出流:发送数据 */ private class MyChannel implements Runnable{ //输入流:接收数据 private DataInputStream dis; //输出流:发送数据 private DataOutputStream dos; //客户端的名称 private String name; //线程运行标识符 private boolean isRunning = true; //构造器 public MyChannel(Socket client) { try { dis = new DataInputStream(client.getInputStream()); dos = new DataOutputStream(client.getOutputStream()); this.name = dis.readUTF();//获取客户端名称 this.send("欢迎加入聊天室");//向本客户端发送此信息 this.sendAll(this.name+"加入了聊天室",true);//向其他的客户端发送该信息 } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dis,dos); all.remove(this);//移除通道自身 } } /** * 接收从客户端发送过来的信息 * @return */ public String receive () { String msg = ""; try { msg = dis.readUTF();//获取读入的信息 send("you say :"+msg); } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dis,dos); all.remove(this);//移除通道自身 } return msg; } /** * 向本客户端发送相关信息 * @param msg */ public void send(String msg) { try { if(null != msg && !msg.equals("")) { dos.writeUTF(msg); dos.flush(); }else { return; } } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dis,dos); all.remove(this);//移除通道自身 } } /** * 向除本客户端以外的其他客户端发送信息 * 根据发送的消息区分是私聊还是群聊 * 在群发的消息中,使用flag区分该消息是服务器的系统消息,还是用户群聊的信息 * @param msg * @param flag */ public void sendAll(String msg,boolean flag) { //约定,如果发送的信息中包含有“@name:.....”,则将name取出,然后与该用户进行私聊 if(msg.startsWith("@") && msg.indexOf(":")>-1) {//私聊 String name = msg.substring(1,msg.indexOf(":"));//获取私聊对象的名称 String contents = msg.substring(msg.indexOf(":")+1);//获取私聊的内容 for(MyChannel other:all) {//遍历所有客户端 if(other.name.equals(name)) {//存在将要私聊的对象 other.send(this.name+"对您悄悄说:"+contents); return; } } this.send("当前聊天室中不存在此用户"); }else {//群聊 if(flag) {//属于系统消息 for(MyChannel other:all) { if(this == other) {//跳过本客户端自身 continue; } //将本客户端发送的数据,发送给其他已经加入聊天的客户 other.send("系统消息:"+msg); } }else { for(MyChannel other:all) { if(this == other) {//跳过本客户端自身 continue; } //将本客户端发送的数据,发送给其他已经加入聊天的客户 other.send(this.name+"对大家说:"+msg); } } } } @Override public void run() { //线程体 while(isRunning) { sendAll(receive(),false); } } } }
解析:
1、正如我们对聊天室的功能分析上,聊天室应该具有群聊和私聊的基本功能。所以我们根据客户端发送的消息进行区分是私聊还是群聊,具体的规则为:服务器获取到客户端发送进来的数据,然后如果该消息以“@XXX:”开头,则获取“@”和“:”中间的名称"XXX",然后在所有客户端中进行搜索名称为“XXX”的客户端,服务器将该消息仅仅转发给客户”XXX“。
2、在我们管理聊天室中的所有客户的时候,我们使用了容器List进行统一管理。但是这里在导入包的时候,一定要注意,此处导入的是容器类包java.util.List。在我们使用自动导包过程时,eclipse给我们的提示中,还有一个是java.awt.List,这个包是java中GUI界面操作的工具类包,千万要注意此处的导包,一旦导错之后,很难检查出错误。
tips:查看源码,可以对比出两个包继承关系以及实现接口之间的差别,进入源码中查询可以看出:
java.util.List中继承关系为:interface List<E> extends Collection<E>,主要是实现相应的容器类;
而java.awt.List继承和实现关系为:List extends Component implements ItemSelectable, Accessible,主要是实现GUI图形界面的工具类
第五步:运行查看一下相关的结果
a客户端控制台信息:

b客户端控制台信息:

c客户端控制台信息:

解析:由于我们使用的是tcp协议,需要客户端先建立连接之后,才可以进行相互通讯传输数据。所以在测试的时候,需要我们首先需要运行服务器,使服务器处于就绪状态,随时接受来自客户端的请求,然后再创建客户端进行操作,否则会报错。我们在测试的时候,创建了3个客户端,分别是“a”、“b”、“c"。在测试的时候,我们使用a客户端给b发送了hello,然后可以在b客户端看到a发送过来的私聊信息,而c客户端界面上没有出现这条信息,所以完成了私发消息的功能。然后使用c客户端发送了信息a beautiful world,该信息属于群发信息,所以出现在了a和b客户端的窗口。然后在c窗口中,对一个不存在的对象d进行发送信息,可以看到服务器返回的信息:当前聊天室中不存在该用户。系统具有一定的容错率。