这周的内容还是蛮有意思的!构建一个聊天室,如果我们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进行发送信息,可以看到服务器返回的信息:当前聊天室中不存在该用户。系统具有一定的容错率。