网络编程
基本概念
什么是计算机网络
- 计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路链接起来,在网络操作系统中,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。
- 计算机网络的作用:资源共享和信息传递。
- 计算机网络的组成:
- 计算机硬件:计算机(大中小型服务器,台式机,笔记本等)、外部设备(路由器、交换机等)、通信线路(双绞线、光纤等)。
- 计算机软件:网络操作系统(Windows 200 Server/Advance Server、Unix、Linux等),网络管理系统(WorkWin、SugaNMS等)、网络通信协议(如:TCP/IP协议栈等)。
- 计算机网络的多台计算机是具有独立功能的,而不是脱离了网络就无法存在的。
什么是网络通信协议
- 通过计算机网络可以实现不同计算机之间的连接与通信,但是计算机网络中实现通信必须有一些
约定
通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准。就像两个人想要顺利沟通就必须使用同一种语言一样,如果一个人只懂英语而另外一个人只懂中文,这样就会造成没有共同语言而无法沟通。 - 国际标准化(ISO,即International Organization for Standardization)定义了通信网络协议的基本框架,被称为OSI(Open System Interconnect,即开发系统互联)模型。OSI模型制定的七层标准模型,分别是:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。
- 虽然国际标准化组织制定了这样一个网络通信协议的模型,但是实际上是TCP/IP网络通信协议。
TCP/IP协议和OSI模型有什么区别
- OSI网络网络通信协议模型是一个参考模型,而TCP/IP协议是事实上的标准。TCP/IP协议参考了OSI模型,但是并没有严格按照OSI的七层标准区划分,而只划分了四层,这样会跟简单点,当划分太多层时,你很难区分某个协议时属于哪个层次的。
- TCP/IP协议和OSI模型也并不冲突,TCP/IP协议中的应用层协议,就对应于OSI中的应用层、表示层、会话层。本质没有多大区别。
- TCP/IP中有两个重要的协议,传输层的TCP协议和互联网络层的IP协议,因此就拿这两个协议做代表,来命名整个协议族了,再说TCP/IP协议时,是指整个协议族。
网络协议的分层
- 由于网络节点之间联系很复杂,在制定协议时,把复杂成分分解成一些简单的蹭分,再讲他们复合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一层,而与下一层不发生关系。
- 把用户应用程序作为最高层,把物理通信线路作为最底层,将其间的协议处理分为若干层,规定每层处理的任务,也规定每层的接口标准。
- OSI模型与TCP/IP模型的对应关系如下:
数据的封装与分解
- 由于用户传输的数据一般都比较大,有的可以达到MB字节,一次性发送出去十分困难,于是就需要把数据分成许多片段,再按照一定的次序发送出去。这个过程就需要对数据进行封装。
- 数据封装(Data Encapsulation)是指将协议数据单元(PDU)封装在一组协议头和协议尾中的过程。在OSI七层参考模型中,每层主要负责与其他机器上的对等层进行通信。该过程是在协议数据单元(PDU)中实现,其中每层的PDU一般由本层的协议头、协议尾和数据封装构成。
数据发送处理过程
- 应用层将数据交给传输层,传输层添加上TCP的控制信息(称为TCP头部),这个数据单元称为段(Segment),加入控制信息的过程称为封装。然后,将段交给网络层。
- 网络层接收到段,在添加上IP头部,这个数据单元称为包(Packet)。然后,将包交给数据链路层。
- 数据链路层接收到包,再添加上MAC头部和尾部,这个数据单元称为帧(Frame)。然后,将帧交给物理层。
- 物理层将接收到的数据转化成为比特流,然后在网线中传送。
数据接收处理过程
- 物理层接收到比特流,经过处理后将数据交给数据链路层。
- 数据链路层将接收到的数据转化为数据帧,再除去MAC头部和尾部,这个除去控制信息的过程称为解封,然后将包交给网络层。
- 网络层接收到包,再去除IP头部,然后将段交给传输层。
- 传输层接收到段,再除去TCP头部,然后将数据交给应用层。
数据传输总结
- 发送方数据处理的方式是从高处到底层,逐层进行封装。
- 接收方数据处理的方式是从底层到高层,逐层进行数据解封装。
- 接收方的每一层只把对该层有意义的数据拿走,或者说每一层只能处理发送方同等层的数据,然后把其余的部分传递给上一层,这就是对等层通信的概念。
IP标识
- 用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。比如互联网的每个服务器都有自己的IP地址,而每个局域网的计算机要通信也要配置IP地址。路由器是连接两个或多个网络的网络设备。
- 目前主流的IP地址是IPV4,但是随着网络规模的不断扩大,IPV4面临着枯竭的危险,所以推出了IPV6。
- IPV4:32位地址,并以8位位一个单位,分为四部分,以点分十进制标识,如:192.168.0.1。因为8位二进制的技术范围是00000000—-11111111,对应十进制的0-255,所以-4.278.4.1是错误的IPV4地址。
- IPV6:128位(16个字节)写成8个16位的无符号整数,每个整数用四个十六进制位表示,每个数之间用冒号(:)分开,如: 3ffe:3201:1401:1280:c8ff:fe4d:db39:1984
注意:
- 127.0.0.1本机地址
- 192.168.0.0—192.168.255.255为私有地址,属于非注册地址,专门为组织机构内部使用(家里的路由器段就是这样的)。
端口
- IP地址用来标识一台计算机,但是一台计算机上可能提供多种网络应用程序,如何来区分这些不同的程序呢?就要用到端口。
- 端口是虚拟的概念,并不是说在主机上真的有若干个端口。通过端口,可以再一个主机上运行多个网络应用程序。端口的标识是一个16位的二进制整数,对应十进制的0-65535.
- IP地址好比每个人的地址(门牌号),端口好比是房间号。必须同时制定IP地址和端口号才能够正确的发送数据。
- 端口默认为80端口。
URL
- 在www上,每一信息资源都有统一且唯一的地址,该地址就叫URL(Uniform Resource Locator),它是www的统一资源定位符。URL有四部分组成:协议、存放资源的主机域名、资源文件名和端口号。如果未制定该端口号,则使用默认的端口。例如http协议的默认端口未80。在浏览器中访问网页时,地址栏显示的地址就是URL。
- http://www.baidu.com:80/index?uname=shsxt&age=18#a 比如这个URL由
协议 + 域名/IP + 端口 + 资源 + ? + 请求参数 + # + 锚点
Socket
- 我们在开发的网络应用程序位于应用层,TCP和UDP属于传输层协议,在应用层如何使用传输层的服务呢?在应用层和传输层之间,则是使用套接Socket来进行分离。
- 套接字就像是传输层为应用层开的一个小口,应用程序通过这个小口向远程发送数据,或者接收远程发来的数据;而这个小口以内,也就是数据进入这个口之后,或者数据从这个口出来之前,是不知道也不需要知道的,也不会关心它如何传输,这属于网络其他层次工作。
- Socket实际是传输层提供给应用层的编程接口。Socket就是应用层与传输层时间的桥梁。使用Socket编程可以开发客户机和服务器应用程序,可以在本地网咯上进行通信,也可通过Internet在全球范围内通信。
TCP协议
TCP(Tramsfer Control Protocol)是面向连接的,所谓面向连接,就是当计算机双方通信时必须经过先建立连接,然后传送数据,最后关闭连接三个过程(Three-way Handshake)。
- 第一步,是请求端(客户端)发送一个包含SYN即同步(Synchronized)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号。
- 第二步,服务器在收到客户端的SYN报文后,将返回一个SYN+ACK的报文,表示客户端的请求被接收,同时TCP序号被加一,ACK即确认(Acknowledgement)。
- 第三步,客户端也返回一个报文ACK给服务器端,同样TCP序列号被加一,到此一个TCP连接完成
UDP协议
基于TCP协议可以建立稳定连接的点对点的通信协议。这种通信方式实时、快速、安全性高,但是很占系统的资源。
在网络传输方式上,还有另一种基于UDP协议的通信方式,也称为数据报通信方式。在这种方式中,每个数据发送单元被统一封装成数据包的方式,发送方将数据报包发送到网络中,数据报包在网络中去寻找它的目的地。
TCP协议和UDP协议的联系和区别
- TCP协议和UDP协议是传输层的两种协议。Socket是传输层供应给应用层的编程接口,所以Socket编程就分为TCP编程和UDP编程两类。
- 在网络通讯时,TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。而UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。
- 这两种传输方式都在实际的网络编程中使用,重要的数据一般使用TCP方式精心数据传输,而大量的非核心数据则可以通过UDP方式传递,在一些程序中甚至结合使用这两种方式进行数据传递。
- 由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。
总结
- TCP是面向连接的,传输数据安全,稳定,效率相对较低。
- UDP是面向无连接的,传输数据不安全,效率较高。
Java网络编程
java为了可移植性,不允许直接操作系统,而是由java.net包来提供网络功能。Java虚拟机负责提供与操作的实际连接。
InetAddress
- 作用:封装计算机的IP地址和DNS(没有端口)。(DNS是Domain Name System,域名系统)
- 特点:这个类没有构造方法。如果得到对象,只能通过静态方法: ```java getLocalHost(); //返回本机主机的地址 getByName(String host); //确定主机名称的IP地址。 getAllByName(String host,byte[] addr); //给定主机的名称,根据系统上配置的名称服务返回器IP地址数组 getByAddress(byte[] addr); //给出原始IP地址的InetAddress对象 getByAddress(String host, byte[] addr); //根据提供的主机名和IP地址创建InetAddress。
// 成员方法 getHostAddress(); //返回文本显示中的IP地址字符串 getAddress(); //返回此 InetAddress对象的原始IP地址。 getHostName(); //获取此IP地址的主机名。
```java
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @author 张辉
* @Description IP 定位一个节点:计算机、路由、通讯设备
* InetAddress: 多个静态方法
* 1. getLocalHost(): 获取本机
* 2. getByName(): 根据域名DNS --》 IP地址
*
* 两个成员方法:
* 1. getHostAddress: 返回地址
* 2. getHostName: 返回计算机名
* @create 2020-05-31 9:22
*/
public class IPTest {
public static void main(String[] args) throws UnknownHostException {
// 使用getLocalHost方法创建InetAddress本机对象
InetAddress addr = InetAddress.getLocalHost();
System.out.println(addr.getHostAddress()); // 返回:192.168.1.149
System.out.println(addr.getHostName()); // 返回:计算机名
// 根据域名得到InetAddress对象
addr = InetAddress.getByName("www.ali.com");
System.out.println(addr.getHostAddress()); // 返回baidu服务器的ip:36.152.44.95
System.out.println(addr.getHostName()); // 输出:www.baidu.com
// 根据ip得到inetAddress对象
addr = InetAddress.getByName("36.152.44.95");
System.out.println(addr.getHostAddress()); // 返回ip:36.152.44.95
System.out.println(addr.getHostName()); //输出ip而不是域名。如果这个IP地址不存在或者DNS服务器不允许进行IP地址和域名的映射,getHostName()方法就直接返回IP地址
}
}
InetSocketAddress
作用:包含IP和端口信息,常用于Socket通信。此类实现IP套接字地址(IP地址 + 端口号),不依赖任何协议。
import java.net.InetSocketAddress;
/**
* @author 张辉
* @Description 端口
* 1. 区分软件
* 2. 2个字节 0-65535 UDP TCP
* 3. 同一个协议端口不能冲突
* 4. 定义端口越大越好
* InetSocketAddress (Socket就是有端口的)
* 1. 构造器
* new InetSocketAddress(地址|域名,端口);
* 2. 方法
* getAddress();
* getPort();
* getHostName();
*
* @create 2020-05-31 10:30
*/
public class PortTest {
public static void main(String[] args) {
// 包含端口
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1",3306);
InetSocketAddress socketAddress1 = new InetSocketAddress("localhost",4000);
System.out.println(socketAddress.getHostName());
// activate.navicat.com// 返回服务的名称
System.out.println(socketAddress1.getAddress());
// localhost/127.0.0.1:返回IP地址
System.out.println(socketAddress1.getPort());
// 4000:返回端口号
}
}
URL
- IP地址唯一表示了Internet上的计算机,而URL则标识了这些计算机上的资源。类URL代表一个统一的资源定位符,它是指向互联网“资源”的指针。资源可以是简单的文件或目录,也可以是对更为复杂的对象的引用,例如对数据库或搜索引擎的查询。
- 为了方便程序员编程使用,JDK中提供了URL类,该类的全名是java.net.URL,有了这样一个类,就可以使用它的各种方法来对URL对象分割、合并等处理。 ```java import java.net.MalformedURLException; import java.net.URL;
/**
- @author 张辉
- @Description URL :统一资源定位器,互联网的三大基石之一(URL,HTTP,HTML),区分定位资源。
- 协议
- 域名、计算机
- 端口:默认80(也就是不写的时候)
- 请求资源
- http://www.baidu.com:80/index?uname=shsxt&age=18#a *
- URL方法
- 获取网络协议:getProtocol();
- 获取域名或者IP:getHost();
- 获取请求的资源(包括请求的参数):getFile();
- 获取请求的资源:getPath();
- 获取请求的端口:getPort();
- 获取请求的参数:getQuery();
- 获取锚点:getRef();
@create 2020-05-31 10:42 */ public class URLTest01 { public static void main(String[] args) throws MalformedURLException {
URL url = new URL("http://www.baidu.com:80/index?uname=shsxt&age=18#acs"); // 获取四个值 System.out.println("协议:" + url.getProtocol()); System.out.println("域名|ip:" + url.getHost()); System.out.println("请求资源1:" + url.getFile()); System.out.println("请求资源2:" + url.getPath()); System.out.println("请求端口:" + url.getPort()); // 参数 System.out.println("参数:" + url.getQuery()); // 锚点 System.out.println("锚点:" + url.getRef()); URL u2 = new URL(url, "2.html"); // 相对路径构建url对象
} } ```
- 网络爬虫
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
/**
* @author 张辉
* @Description 网络爬虫原理
* @create 2020-05-31 11:01
*/
public class SpiderTest01 {
public static void main(String[] args) throws Exception {
// 获取URL
//URL url = new URL("https://www.jd.com");
URL url = new URL("https://www.dianping.com");
// 下载资源
InputStream is = url.openStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is,"UTF-8"));
String msg = null;
while ((msg = br.readLine()) != null) {
System.out.println(msg);
}
br.close();
// 分析
// 筛选
}
}
//____________________________________
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* @author 张辉
* @Description 网络爬虫原理 + 模拟浏览器
* @create 2020-05-31 11:01
*/
public class SpiderTest02 {
public static void main(String[] args) throws Exception {
// 获取URL
URL url = new URL("https://www.dianping.com");
// 下载资源
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36");
// 这儿是在网页的request中获取的。
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(),"UTF-8"));
String msg = null;
while ((msg = br.readLine()) != null) {
System.out.println(msg);
}
br.close();
// 分析
// 筛选
}
}
基于TCP协议的Socket编程和通信
在网络通讯中,第一次主动发起通讯的程序被称为客户端(Client)程序,简称客户端,而在第一次通讯中等待连接的程序被称作服务端(Server)程序,简称服务器。一但通讯简历,则客户端和服务器端完全一样,没有本质的区别。
“请求-响应”模式
- Socket类:发送TCP消息。
- ServerSocket类:创建服务器。
- 套接字是一种进程间的数据交换的机制。这些进程既可以在同一个机器上,也可以在网络连接的不同机器上。换句话说,套接字起到通信端点的作用。单个套接字是一个端点,而一对套接字则构成一个双向通信管道,是非关联进程可以在本地或通过网络进行数据交换。一旦建立套接字连接,数据即可相同或不同的系统同中双向或单向发送,知道其中一个端点关闭连接。套集字与主机地址和端口地址相关联。主机地址就是客户端或服务器程序所在的主机的IP地址。端口地址是指客户端或服务器程序使用的主机的通讯端口。
- 在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样,客户端和服务器通过套接字所建立的连接使用输入输出流进行通信。
- TCP/IP套接字是最可靠的双向流协议,使用TCP/IP 可以发送任意数量的数据。
- 实际上,套接字只是计算机上已编号的端口。如果发送方和接收方计算机确定好端口,他们就可以通信了。
TCP/IP 通信连接地简单过程
- 位于A计算机上的TCP/IP软件向B计算机发送包含端口号的信息,B计算机的TCP/IP软件接收到该消息,并进行检查,查看它是否有它知道的程序正在该端口上接收消息。如果有,他就将该消息交给这个程序。
- 要是程序有效地运行,就必须有一个客户端和一个服务器。
通过Socket的编程顺序
- 创建服务器ServerSocket,在创建时,定义ServerSocket的监听端口(在这个端口接收客户端发来的消息)。
- ServerSocket调用accept()方法,使之处于阻塞状态。
- 创建客户端Socket,并设置服务器的IP及端口。
- 客户端发出连接请求,建立连接。
- 分别取得服务器和客户端Socket的InputStream和OutputStream。
- 利用Socket和ServerSocket进行数据传输。
- 关闭流及Socket。
TCP双向通讯实例
// ----------------------Server--------------------------
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author 张辉
* @Description 服务端(双向)
* @create 2020-06-02 17:50
*/
public class LoginTwoWayServer2 {
public static void main(String[] args) throws IOException {
System.out.println("-----------Server---------------");
// 1. 指定端口,使用ServerSocket创建服务器
ServerSocket server = new ServerSocket(8888);
// 2. 阻塞式等待
Socket client = server.accept();
System.out.println("一个客户端建立了连接");
// 3. 操作输入输出流操作
DataInputStream dis = new DataInputStream(client.getInputStream());
// 获取通过指定端口进来的数据
String data = dis.readUTF();
String uname = "";
String upassword = "";
// 分析
String[] dataArray = data.split("&");
for (String info: dataArray) {
String[] userInfo = info.split("=");
if (userInfo[0].equals("uname")) {
System.out.println("你的用户名:" + userInfo[1]);
uname = userInfo[1];
} else if (userInfo[0].equals("upassword")) {
System.out.println("你的密码为:" + userInfo[1]);
upassword = userInfo[1];
}
}
// 输出
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
// 通过连接将数据返回
if (uname.equals("zhangsan") && upassword.equals("123456")) {
// 成功
dos.writeUTF("登陆成功!欢迎使用!");
} else {
// 失败
dos.writeUTF("用户名或密码错误!");
}
dos.flush();
// 4. 释放资源
dos.close();
dis.close();
client.close();
}
}
//-------------------------Client---------------------------
import java.io.*;
import java.net.Socket;
/**
* @author 张辉
* @Description 客户端(双向)
* @create 2020-06-02 17:50
*/
public class LoginTwoWayClient2 {
public static void main(String[] args) throws IOException {
System.out.println("------------------Client----------------------");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入用户名:");
String uname = br.readLine();
System.out.println("请输入密码:");
String upassword = br.readLine();
//1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
Socket client = new Socket("localhost",8888);
//2. 操作:输入输出流操作
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
dos.writeUTF("uname=" + uname + "&" + "upassword=" + upassword);
dos.flush();
DataInputStream dis = new DataInputStream(client.getInputStream());
String result = dis.readUTF();
System.out.println(result);
//3. 释放资源
dos.close();
client.close();
}
}
UDP通讯的实现
DatagramSocket:用于发送或接收数据报包
- 当服务器要想客户端发送数据时,需要在服务器端产生一个DatagramSocket对象。服务器端的DatagramSocket将DatagramPacket发送到网络上,然后被客户端Datagramsocket接收。
- DatagramSocket有两种常用的构造函数。一种是无需任何参数的,常用于客户端;另一种需要指定端口,常用于服务器端。如下所示:
DatagramSocket():
构造数据报套接字并将其绑定到本地主机上的任何可用的端口。DatagramSocket(int port):
创建数据报套接字并将其绑定到本地主机上的指定端口。
常用方法:
send(DatagramSocket p)
: 从此套接字发送数据报包。receive(DatagramPacket p)
:从此套接字接收数据报包。close()
:关闭此数据包。
DatagramPacket:数据容器(封包)的作用
此类表示数据报包,数据报包用来实现封包的功能。
常用方法:
- DatagramPacket(byte[] buf, int len):构造数据报包,用来接收长度为length的数据包。
- DatagramPacket(byte[] buf, int length, InetAddress address, int port):构造数据包,用来将长度为length的包发送到指定主机上的指定端口号。
- getAddress():获取发送或接受方计算机的IP地址,此数据包将要发往该机器或者是从该机器接收到的。
- getData():获取发送或接受的数据。
- setData(byte[] buf):设置发送的数据。
UDP通信变成基本步骤
- 创建客户端的DatagramSocket,创建时,定义客户端的监听端口。
- 创建服务器端的DatagramScoket,创建时,定义服务器的监听端口。
- 在服务器端定义DatagramPacket对象,封装等待发送的数据包。
- 客户端将数据报包发出去。
- 服务端接收数据报包。
UDP双向通讯实例
// ----------------------多线程,发送端------------------------
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;
import java.nio.Buffer;
/**
* @author 张辉
* @Description 多线程发送端,使用面向对象的封装
* @create 2020-06-02 15:25
*/
public class TalkSend implements Runnable {
private DatagramSocket client;
private BufferedReader reader;
private String toIP;
private int toPort;
public TalkSend(int port, String toIP, int toPort) {
this.toIP = toIP;
this.toPort = toPort;
try {
client = new DatagramSocket(port);
reader = new BufferedReader(new InputStreamReader(System.in));
} catch (SocketException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
String data = null;
try {
data = reader.readLine();
byte[] datas = data.getBytes();
// 封装成DatagramPacket 包裹,需要指定目的地
DatagramPacket packet = new DatagramPacket(datas,0,datas.length,
new InetSocketAddress(this.toIP,this.toPort));
client.send(packet);
if (data.equals("bye")) {
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 释放资源
client.close();
}
}
// ----------------------多线程,接收端------------------------
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/**
* @author 张辉
* @Description 接收端,使用面向对象封装
* @create 2020-06-02 15:26
*/
public class TalkReceive implements Runnable{
private DatagramSocket server ;
private String from;
public TalkReceive(int port, String from) {
this.from = from;
try {
server = new DatagramSocket(port);
} catch (SocketException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
byte[] contaienr = new byte[1024 * 60];
DatagramPacket packet = new DatagramPacket(contaienr, 0, contaienr.length);
try {
server.receive(packet);
byte[] datas = packet.getData();
int len = packet.getLength();
String data = new String(datas, 0 ,len);
System.out.println(from + " --> " + data);
if (data.equals("bye")) {
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
server.close();
}
}
// ----------------------使用多线程,老师端------------------------
/**
* @author 张辉
* @Description 加入多线程,实心双向交流,模拟在线咨询
* @create 2020-06-02 16:24
*/
public class TalkTeacher {
public static void main(String[] args) {
new Thread(new TalkReceive(9999, "学生")).start();// 接收
new Thread(new TalkSend(5555, "localhost", 8888)).start();// 发送
}
}
// ----------------------使用多线程,学生端------------------------
/**
* @author 张辉
* @Description 加入多线程,实心双向交流,模拟在线咨询
* @create 2020-06-02 16:22
*/
public class TalkStudent {
public static void main(String[] args) {
new Thread(new TalkSend(7777, "localhost", 9999)).start();// 发送
new Thread(new TalkReceive(8888, "老师")).start();// 接收
}
}
总结
- 端口时虚拟的概念,并不是说在主机上真的有若干个端口。
- 在www上,每一信息资源都有统一且唯一的地址,该地址就叫URL(Uniform Resorce Locator),他是www的统一资源定位符。
- TCP与UDP的区别
- TCP 是面向连接的,传输数据俺去那,稳定,效率相对低。
- UDP是面向无连接的,传输数据不安全,效率较高。
- Socket通信是一种基于TCP协议,建立稳定连接的点对点通信。
- 网络编程是由java.net包来提供网络功能。
- InetAddress:封装计算机的IP地址和DNS(没有端口信息)。
- InetSocketAddress:包含IP和端口,常用于Socket通信。
- URL:以使用它的个助攻方法来对URL对象进行分割、合并等处理。
- 基于TCP协议的Socket编程和通信
- “请求-响应”模式:
—Socket类:发送TCP消息。
—ServerSocket类:创建服务器。
- “请求-响应”模式:
- UDP通讯的实现
- DatgramSocket:用于发送或接收数据包。
- 常用方法:send(),receive(),close()。
- DatagramPacket:数据容器(封包)的作用
- 常用方法:构造方法、getAddress(获取发送或接收方计算机的IP地址)、getData(获取发送或接收的数据)、setData(设置发送的数据)。
基于TCP的聊天室
流关闭工具类
import java.io.Closeable;
import java.io.IOException;
/**
* @author 张辉
* @Description 释放资源----》工具类
* @create 2020-06-02 22:02
*/
public class ZhUtils {
public static void close(Closeable... targets) {
for (Closeable target : targets) {
try {
if (target != null) {
target.close();
}
} catch (IOException e) {
System.out.println("关闭流(工具类)出错");
}
}
}
}
聊天室服务端
// 服务端就是接收信息,并且转发信息。
// 管理员功能:存一个表,用来存储管理员的信息,在以后的禁言和踢人中使用。
// 禁言功能:禁言就是在管理员设置了禁言之后,一段时间内,将某人的账号限制,发的消息,不向其他人转发。
// 踢人功能:直接关闭流。
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author 张辉
* @Description 在线聊天室:服务端
* 版本1.0:目标:实现一个客户可以正常发消息
* 版本1.1:目标:发送多条消息
* 版本2.0:目标:实现多个用户发送多条消息 (问题:客户必须等待之前的客户退出,才能够使用)
* 版本2.1:目标:使用多线程,解决2.0版本的问题(问题:线程代码太多,不好维护,客户端读写没有分开,必须先写后读)
* 版本3.0:目标:使用oop,解决版本2.1的问题
* 版本4.0:目标:加入容器,实现群聊
* 版本5.0:目标:实现简单地私聊
* @create 2020-06-02 20:54
*/
public class Chat {
private static CopyOnWriteArrayList<Channel> all = new CopyOnWriteArrayList<Channel>();
// CopyWiterArrayList 可以一边读一边写,(解决并发)
public static void main(String[] args) throws IOException {
System.out.println("-----------------Server-------------------");
// 1. 指定端口 使用ServerSocket创建服务器
ServerSocket server = new ServerSocket(8888);
while (true) {
// 2. 阻塞式等待连接 accept
Socket client = server.accept(); // 监听端口
System.out.println("一个客户端建立了连接");
Channel c = new Channel(client);
all.add(c); // 使用容器管理所有成员
new Thread(c).start();
}
}
static class Channel implements Runnable {
private DataInputStream dis;
private DataOutputStream dos;
private Socket client;
private boolean isRunning;
private String name;
public Channel(Socket client) {
this.client = client;
try {
dis = new DataInputStream(client.getInputStream());
dos = new DataOutputStream(client.getOutputStream());
isRunning = true;
// 获取名称
name = receive();
// 欢迎来到
this.send("欢迎来到!");
sendOthers(this.name + "来到了聊天室", true);
} catch (IOException e) {
System.out.println("初始化Channel出错");
release();
}
}
// 接收消息
private String receive() {
String msg = ""; // 在处理之前先进行初始化,就可以避免空指异常
try {
msg = dis.readUTF();
} catch (IOException e) {
System.out.println("接收消息receive出错");
release();
}
return msg;
}
// 发送消息
private void send(String msg) {
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
System.out.println("发送消息send出错");
release();
}
}
/**
* 群聊:获取自己的消息,发给其他人
* 私聊:约定数据格式:@xxx:msg
*
* @param msg
*/
private void sendOthers(String msg, boolean isSys) {
boolean isPrivate = msg.startsWith("@");
if (isPrivate) {
// 私聊
int idx = msg.indexOf(":"); // 返回“:”第一次出现的地方
// 获取目标和数据
String targetName = msg.substring(1, idx);
msg = msg.substring(idx + 1);
for (Channel other:all) {
if (other.name.equals(targetName)) {
// 目标找到
other.send(this.name + "悄悄地对你说:" + msg); // 群聊消息
}
}
} else {
// 群聊
for (Channel other : all) {
if (other == this) {
// 自己
continue;
}
if (!isSys) {
other.send(this.name + "说:" + msg); // 群聊消息
} else {
other.send(msg); // 系统消息
}
}
}
}
// 释放资源
private void release() {
this.isRunning = false;
ZhUtils.close(dos, dis, client);
all.remove(this);
sendOthers(this.name + "离开了大家庭!", true);
}
@Override
public void run() {
while (isRunning) {
String msg = receive(); // 接收消息
if (!msg.equals("")) {
//send(msg); // 发送消息
sendOthers(msg, false);
}
}
}
}
}
聊天室客户端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* @author 张辉
* @Description 在线聊天室:客户端
* 版本1.0:目标:实现一个客户可以正常发消息
* 版本1.1:目标:发送多条消息
* 版本2.0:目标:实现多个用户发送多条消息 (问题:客户必须等待之前的客户退出,才能够使用)
* 版本2.1:目标:使用多线程,解决2.0版本的问题(问题:线程代码太多,不好维护,客户端读写没有分开,必须先写后读)
* 版本3.0:目标:使用oop,解决版本2.1的问题
* 版本4.0:目标:加入容器,实现群聊
* @create 2020-06-02 20:54
*/
public class Client {
public static void main(String[] args) throws IOException {
System.out.println("-----------------Client-------------------");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入用户名:");
String name = br.readLine();
// 1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
Socket client = new Socket("localhost", 8888);
// 2. 客户端发送消息
new Thread(new Send(client, name)).start();
new Thread(new Receive(client)).start();
}
}
发送类
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* @author 张辉
* @Description 发送端:使用多线程封装了发送端
* 1. 发送消息
* 2. 从控制台获取消息
* 3. 释放资源
* 4. 重写run()
* @create 2020-06-02 22:23
*/
public class Send implements Runnable {
private BufferedReader console;
private DataOutputStream dos;
private Socket client;
private boolean isRunning;
private String name;
public Send(Socket client, String name) {
this.client = client;
this.name = name;
console = new BufferedReader(new InputStreamReader(System.in));
try {
dos = new DataOutputStream(client.getOutputStream());
isRunning = true;
//发送名称
send(name);
} catch (IOException e) {
System.out.println("初始化Send出错");
this.release();
}
}
@Override
public void run() {
while (isRunning) {
String msg = getStrFromConsole();
if (!msg.equals("")) {
send(msg);
}
}
}
private void send(String msg) {
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
System.out.println("客户端发送消息send出错");
release();
}
}
/**
* 从控制台获取消息
* @return
*/
private String getStrFromConsole() {
try {
return console.readLine();
} catch (IOException e) {
e.printStackTrace();
System.out.println("从控制台获取输入出错");
}
return "";
}
private void release() {
this.isRunning = false;
ZhUtils.close(dos, client);
}
}
接收类
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
/**
* @author 张辉
* @Description 接收端:使用多线程封装了接收端
* 1. 接收消息
* 3. 释放资源
* 4. 重写run()
* @create 2020-06-02 22:23
*/
public class Receive implements Runnable {
private DataInputStream dis ;
private Socket client;
private boolean isRunning;
public Receive(Socket client) {
this.client = client;
try {
dis = new DataInputStream(client.getInputStream()); // 从服务器get消息
isRunning = true;
} catch (IOException e) {
System.out.println("客户端初始化Receive出错");
release();
}
}
private String receive (){
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
System.out.println("客户端接收消息receive出错");
release();
}
return msg;
}
@Override
public void run() {
while (isRunning) {
String msg = receive();
if (!msg.equals("")) {
System.out.println(msg);
}
}
}
private void release() {
this.isRunning = false;
ZhUtils.close(dis, client);
}
}