目录
一、网络编程基本概念(了解)
1.1 计算机网络
计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。
从其中我们可以提取到以下内容:
计算机网络的作用:资源共享和信息传递。
计算机网络的组成:
- 计算机硬件:计算机(大中小型服务器,台式机、笔记本等)、外部设备(路由器、交换机等)、通信线路(双绞线、光纤等)。
- 计算机软件:网络操作系统(Windows 2000 Server/Advance Server、Unix、Linux等)、网络管理软件(WorkWin、SugarNMS等)、网络通信协议(如TCP/IP协议栈等)。
1.2 网络通信协议
1.2.1什么是网络通信协议
通过计算机网络可以实现不同计算机之间的连接与通信,但是计算机网络中实现通信必须有一些约定即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准。
国际标准化组织(ISO,即International Organization for Standardization)定义了网络通信协议的基本框架,被称为OSI(Open System Interconnect,即开放系统互联)模型。要制定通讯规则,内容会很多,比如要考虑A电脑如何找到B电脑,A电脑在发送信息给B电脑时是否需要B电脑进行反馈,A电脑传送给B电脑的数据格式又是怎样的?内容太多太杂,所以OSI模型将这些通讯标准进行层次划分,每一层次解决一个类别的问题,这样就使得标准的制定没那么复杂。OSI模型制定的七层标准模型,分别是:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。
1.2.2 OSI七层协议模型
1.2.3网络协议的分层
虽然国际标准化组织制定了这样一个网络通信协议的模型,但是实际上互联网通讯使用最多的网络通信协议是TCP/IP网络通信协议。
TCP/IP 模型,也是按照层次划分,共四层:应用层,传输层,网络层,网络接口层(物理+数据链路层)。
OSI模型与TCP/IP模型的对应关系:
1.3数据的封装和解封
数据封装(Data Encapsulation)是指将协议数据单元(PDU)封装在一组协议头和协议尾中的过程。在OSI七层参考模型中,每层主要负责与其它机器上的对等层进行通信。该过程是在协议数据单元(PDU)中实现的,其中每层的PDU一般由本层的协议头、协议尾和数据封装构成。
数据发送处理过程
- 应用层将数据交给传输层,传输层添加上TCP的控制信息(称为TCP头部),这个数据单元称为段(Segment),加入控制信息的过程称为封装。然后,将段交给网络层。
- 网络层接收到段,再添加上IP头部,这个数据单元称为包(Packet)。然后,将包交给数据链路层。
- 数据链路层接收到包,再添加上MAC头部和尾部,这个数据单元称为帧(Frame)。然后,将帧交给物理层。
- 物理层将接收到的数据转化为比特流,然后在网线中传送。
数据接收处理过程
- 物理层接收到比特流,经过处理后将数据交给数据链路层。
- 数据链路层将接收到的数据转化为数据帧,再除去MAC头部和尾部,这个除去控制信息的过程称为解封,然后将包交给网络层。
- 网络层接收到包,再除去IP头部,然后将段交给传输层。
- 传输层接收到段,再除去TCP头部,然后将数据交给应用层。
从以上传输过程中,可以总结出以下规则:
- 发送方数据处理的方式是从高层到底层,逐层进行数据封装。
- 接收方数据处理的方式是从底层到高层,逐层进行数据解封。
接收方的每一层只把对该层有意义的数据拿走,或者说每一层只能处理发送方同等层的数据,然后把其余的部分传递给上一层,这就是对等层通信的概念。
数据封装与解封:
数据封装
数据解封
1.4 IP地址
IP地址
IP是Internet Protocol Address,即"互联网协议地址"。
用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。 比如互联网的每个服务器都要有自己的IP地址,而每个局域网的计算机要通信也要配置IP地址。
路由器是连接两个或多个网络的网络设备。
IP地址分类:
类别 | 最大网络数 | IP地址范围 | 单个网段最大主机数 | 私有IP地址范围 |
---|---|---|---|---|
A | 126(2^7-2) | 1.0.0.1-127.255.255.254 | 16777214 | 10.0.0.0-10.255.255.255 |
B | 16384(2^14) | 128.0.0.1-191.255.255.254 | 65534 | 172.16.0.0-172.31.255.255 |
C | 2097152(2^21) | 192.0.0.1-223.255.255.254 | 254 | 192.168.0.0-192.168.255.255 |
目前主流使用的IP地址是IPV4,但是随着网络规模的不断扩大,IPV4面临着枯竭的危险,所以推出了IPV6。
IPV4,采用32位地址长度,只有大约43亿个地址,它只有4段数字,每一段最大不超过255。随着互联网的发展,IP地址不够用了,在2019年11月25日IPv4位地址分配完毕。
IPv6采用128位地址长度,几乎可以不受限制地提供地址。按保守方法估算IPv6实际可分配的地址,整个地球的每平方米面积上仍可分配1000多个地址。
IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1
实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。
IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334
公有地址
公有地址(Public address)由Inter NIC(Internet Network Information Center互联网信息中心)负责。这些IP地址分配给注册并向Inter NIC提出申请的组织机构。通过它直接访问互联网。
私有地址
私有地址(Private address)属于非注册地址,专门为组织机构内部使用。
以下列出留用的内部私有地址
A类 10.0.0.0--10.255.255.255
B类 172.16.0.0--172.31.255.255
C类 192.168.0.0--192.168.255.255
注意事项
- 127.0.0.1 本机地址
- 192.168.0.0--192.168.255.255为私有地址,属于非注册地址,专门为组织机构内部使用
1.5 端口
端口号用来识别计算机中进行通信的应用程序。因此,它也被称为程序地址。
一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地进行数据传输。
总结
- IP地址好比每个人的地址(门牌号),端口好比是房间号。必须同时指定IP地址和端口号才能够正确的发送数据。
- IP地址好比为电话号码,而端口号就好比为分机号。
端口分配
端口是虚拟的概念,并不是说在主机上真的有若干个端口。通过端口,可以在一个主机上运行多个网络应用程序。 端口的表示是一个16位的二进制整数,对应十进制的0-65535。
操作系统中一共提供了0~65535可用端口范围。
按端口号分类:
公认端口(Well Known Ports):从0到1023,它们紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务的协议。例如:80端口实际上总是HTTP通讯。
注册端口(Registered Ports):从1024到65535。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
1.6 URL
URL作用:
URL(Uniform Resource Locator),是互联网的统一资源定位符。用于识别互联网中的信息资源。通过URL我们可以访问文件、数据库、图像、新闻等。
在互联网上,每一信息资源都有统一且唯一的地址,该地址就叫URL,URL由4部分组成:协议 、存放资源的主机域名、资源文件名和端口号。如果未指定该端口号,则使用协议默认的端口。例如http 协议的默认端口为 80。 在浏览器中访问网页时,地址栏显示的地址就是URL。
在java.net包中提供了URL类,该类封装了大量复杂的涉及从远程站点获取信息的细节。
1.7 Socket
我们开发的网络应用程序位于应用层(操作系统),TCP和UDP属于传输层协议,在应用层如何使用传输层的服务呢?在应用层和传输层之间,则是使用套接字Socket来进行分离。
套接字就像是传输层为应用层开的一个小口,应用程序通过这个小口向远程发送数据,或者接收远程发来的数据;而这个小口以内,也就是数据进入这个口之后,或者数据从这个口出来之前,是不知道也不需要知道的,也不会关心它如何传输,这属于网络其它层次工作。
Socket实际是传输层供给应用层的编程接口。Socket就是应用层与传输层之间的桥梁。使用Socket编程可以开发客户机和服务器应用程序,可以在本地网络上进行通信,也可通过Internet在全球范围内通信。
1.8 TCP协议和UDP协议
1.8.1 TCP协议
TCP(Transmission Control Protocol,传输控制协议)。TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。
TCP在建立连接时又分三步走:
第一步,是请求端(客户端)发送一个包含SYN即同步(Synchronize)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号。
第二步,服务器在收到客户端的SYN报文后,将返回一个SYN+ACK的报文,表示客户端的请求被接受,同时TCP序号被加一,ACK即确认(Acknowledgement)。
第三步,客户端也返回一个确认报文ACK给服务器端,同样TCP序列号被加一,到此一个TCP连接完成。然后才开始通信的第二步:数据处理。
这就是所说的TCP的三次握手(Three-way Handshake)。
1.8.2 UDP协议
UDP(User Data Protocol,用户数据报协议)
UDP是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。
UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP
1.8.3 两者区别
这两种传输方式都在实际的网络编程中使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则可以通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据传递。
由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。
UDP | TCP | |
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 |
总结
- TCP是面向连接的,传输数据安全,稳定,效率相对较低。
- UDP是面向无连接的,传输数据不安全,效率较高。
二、Java网络编程中的常用类
Java为了跨平台,在网络应用通信时是不允许直接调用操作系统接口的,而是由java.net包来提供网络功能。而是通过java.net包开发。
2.1 InetAddress类的使用
作用:封装计算机的IP地址和DNS(没有端口信息)
注:DNS是Domain Name System,域名系统。
特点:
这个类没有构造方法。如果要得到对象,只能通过静态方法:getLocalHost()、getByName()、 getAllByName()、 getAddress()、getHostName()
2.1.1 获取本机信息
package cn.it.bz.Socket;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetTest {
public static void main(String[] args) throws UnknownHostException {
//实例化本机InetAddress对象
InetAddress localHost = InetAddress.getLocalHost();
//获取当前计算机的IP
System.out.println(localHost.getHostAddress());
//获取计算机名
System.out.println(localHost.getHostName());
}
}
2.1.2 根据域名获取计算机的信息
根据域名获取计算机信息时需要使用getByName(“域名”)方法创建InetAddress对象。
package cn.it.bz.Socket;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetTest2 {
public static void main(String[] args) throws UnknownHostException {
//根据域名获取InetAddress对象
InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
//获取IP
System.out.println(inetAddress.getHostAddress());
//获取计算机(服务器)名字
System.out.println(inetAddress.getHostName());
}
}
2.1.3根据IP地址获取计算机的信息
package cn.it.bz.Socket;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetTest3 {
public static void main(String[] args) throws UnknownHostException {
//根据计算机IP获取InetAddress对象
InetAddress inetAddress = InetAddress.getByName("39.156.66.18");
System.out.println(inetAddress.getHostName());
}
}
2.2 InetSocketAddress类的使用
作用:包含IP和端口信息,常用于Socket通信。此类实现 IP 套接字地址(IP 地址 + 端口号),不依赖任何协议。
InetSocketAddress相比较InetAddress多了一个端口号,端口的作用:一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现。
那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP 地址与网络服务的关系是一对多的关系。实际上是通过“IP地址+端口号”来区分不同的服务的。
package cn.it.bz.Socket;
import java.net.InetSocketAddress;
public class InetSocketTest {
public static void main(String[] args) {
InetSocketAddress inetSocketAddress = new InetSocketAddress("www.baidu.com",80);
//获取IP地址 getAddress()先返回一个InetAddress对象才能获取到IP
System.out.println(inetSocketAddress.getAddress().getHostAddress());
//获取计算机域名
System.out.println(inetSocketAddress.getHostName());
}
}
2.3 URL的使用
IP地址标识了Internet上唯一的计算机,而URL则标识了这些计算机上的资源。 URL 代表一个统一资源定位符,它是指向互联网“资源”的指针。资源可以是简单的文件或目录,也可以是对更为复杂的对象的引用,例如对数据库或搜索引擎的查询。
为了方便程序员编程,JDK中提供了URL类,该类的全名是java.net.URL,有了这样一个类,就可以使用它的各种方法来对URL对象进行分割、合并等处理。
package cn.it.bz.Socket;
import java.net.MalformedURLException;
import java.net.URL;
public class UrlTest {
public static void main(String[] args) throws MalformedURLException {
//创建URL对象
URL url = new URL("https://www.itbaizhan.com/search.html?kw=java");
System.out.println("获取与此URL相关联协议的默认端口:"+url.getDefaultPort());
System.out.println("访问资源:"+url.getFile());
System.out.println("主机名"+url.getHost());
System.out.println("访问资源路径:"+url.getPath());
System.out.println("协议:"+url.getProtocol());
System.out.println("参数部分:"+url.getQuery());
}
}
https协议默认端口是443,http协议默认是80。
2.3.1 通过URL实现最简单的网络爬虫
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
public class UrlTest2 {
public static void main(String[] args) throws MalformedURLException {
URL url = new URL("https://www.itbaizhan.com/");
//url.openStream()返回的是字节流
try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(url.openStream()))) {
StringBuilder stringBuilder = new StringBuilder();
String temp = "";
while ((temp = bufferedReader.readLine()) != null){
stringBuilder.append(temp);
}
System.out.println(stringBuilder);
}catch (IOException e){
e.printStackTrace();
}
}
}
三、TCP通信的实现
3.1 TCP通信实现原理
TCP协议是面向的连接的,在通信时客户端与服务器端必须建立连接。在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client)程序,简称客户端,而在第一次通讯中等待连接的程序被称作服务器端(Server)程序,简称服务器。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。
请求-响应”模式:
- Socket类:发送TCP消息(实际上就是客户端对象)。
- ServerSocket类:创建服务器(实际上就是服务器对象)。
套接字Socket是一种进程间的数据交换机制。这些进程既可以在同一机器上,也可以在通过网络连接的不同机器上。换句话说,套接字起到通信端点的作用。单个套接字是一个端点,而一对套接字则构成一个双向通信信道,使非关联进程可以在本地或通过网络进行数据交换。一旦建立套接字连接,数据即可在相同或不同的系统中双向或单向发送,直到其中一个端点关闭连接。套接字与主机地址和端口地址相关联。主机地址就是客户端或服务器程序所在的主机的IP地址。端口地址是指客户端或服务器程序使用的主机的通信端口。
在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样,客户端和服务器通过套接字所建立的连接使用输入输出流进行通信。
TCP/IP套接字是最可靠的双向流协议,使用TCP/IP可以发送任意数量的数据。
实际上,套接字只是计算机上已编号的端口。如果发送方和接收方计算机确定好端口,他们就可以通信了。
客户端与服务器端的通信关系图:
TCP/IP通信连接的简单过程:
位于A计算机上的TCP/IP软件向B计算机发送包含端口号的消息,B计算机的TCP/IP软件接收该消息,并进行检查,查看是否有它知道的程序正在该端口上接收消息。如果有,他就将该消息交给这个程序。要使程序有效地运行,就必须有一个客户端和一个服务器。
通过Socket的编程顺序:
- 创建服务器ServerSocket,在创建时,定义ServerSocket的监听端口(在这个端口接收客户端发来的消息)
- ServerSocket调用accept()方法,使之处于阻塞状态。accept方法监听服务器和客户端之间是否连接,一旦连接就基于客户端返回一个对应Socket对象,客户端和服务器之间的连接就是两个Socket对象之间的连接。
- 创建客户端Socket,并设置服务器的IP及端口。
- 客户端发出连接请求,建立连接。先通过三次握手建立客户端和服务器之间TCP协议的连接,然后通过Socket建立客户端和服务器之间的连接。客户端和服务器之间的连接是建立在协议连接基础之上的。(还可以这么理解。协议之间的连接好比打开了两个小区之间交流的通道,通过Socket建立的客户端和服务器之间的连接好比是建立了两个小区中住户之间的连接)
- 分别取得服务器和客户端Socket的InputStream和OutputStream。
- 利用Socket和ServerSocket进行数据传输。
- 关闭流及Socket。
3.1.1 创建服务器
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
//服务端
public class BasicSocketServer {
public static void main(String[] args) {
System.out.println("服务器启动,等待监听……");
//创建ServerSocket对象
try(ServerSocket serverSocket = new ServerSocket(8888);
//监听8888端口,此时当前线程处于堵塞状态,当监听到客户端连接到该端口时解除阻塞状态。
//连接成功返回的就是客户端的Socket对象
Socket socket = serverSocket.accept();
//获取Socket中的输入流,以获取客户端发送到服务端的数据
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
) {
//读取客户端发送的数据
bufferedReader.readLine();
}catch (Exception e){
e.printStackTrace();
System.out.println("服务器启动失败……");
}
}
}
public class ServerSocket implements java.io.CloseableServerSocket 实现了 Closeable接口会自动关闭流对象,前提是使用try-with-resource语法。
3.1.2 创建客户端
package cn.it.bz.Socket;
import java.io.PrintWriter;
import java.net.Socket;
//客户端
public class BasicSocketClient {
public static void main(String[] args) {
//创建客户端对象
try(Socket socket = new Socket("127.0.0.1",8888);
//创建向服务端发送数据的输出流对象
PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
) {
printWriter.println("服务端!你好!");
printWriter.flush();
}catch (Exception e){
e.printStackTrace();
}
}
}
Socket 也实现了自动关闭的接口,前提是使用try-with-resource语法,但是不要忘了刷新。
3.1.3 ”丑话“说在前头
在socket中不管是客户端还是服务端在向输出流写入数据时,一般要调用printWriter.println();方法,而不是printWriter.write();方法。原因是:
在使用 Socket 进行网络通信时,如果需要向外写数据,可以通过 OutputStream 对象来实现。但是,即使调用了 write() 方法将数据写入到输出流中,并不能保证对方能够正常接收到该数据。这主要是因为在 TCP/IP 协议栈中,发送端和接收端之间的数据传输是通过网络缓冲区实现的。当发送端调用 write() 方法将数据写入到输出流中后,并不会立即将数据发送给接收端,而是先将数据存放到输出缓冲区中等待发送。当调用 write() 方法写入数据时,并不会同时确保该数据已经被成功接收,因此在网络中可能会发生丢包或拥塞等情况,从而导致发送数据失败或接收数据错误。
3.2 TCP单项通信
单向通信是指通信双方中,一方固定为发送端,一方则固定为接收端。
3.2.1创建服务端
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
//服务端
public class OneWaySocketServer {
public static void main(String[] args) {
System.out.println("服务器启动,开始监听……");
try(//实例化服务器对象
ServerSocket serverSocket = new ServerSocket(8888);
//取得Socket
Socket socket = serverSocket.accept();
//获取字符缓冲输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()))
) {
System.out.println("连接成功^_^");
//一直接受客户端的消息
while (true){
String s = bufferedReader.readLine();
if ("关闭".equals(s)){
break;
}
System.out.println("客户端传递来的数据:"+s);
}
}catch (Exception e){
e.printStackTrace();
System.out.println("服务器启动失败-_-");
}
}
}
3.2.2 创建客户端
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
//客户端
public class OneWaySocketClient {
public static void main(String[] args) {
//获取与服务端对应的Socket对象
try(Socket socket = new Socket("127.0.0.1",8888);
//通过与服务端对应的Socket对象获取输出流对象
PrintWriter pw = new PrintWriter(socket.getOutputStream());
//通过与服务端对应的Socket对象获取输入流对象
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())))
{
//创建键盘输入对象
System.out.println("输入数据,输入exit退出程序");
Scanner scanner = new Scanner(System.in);
while(true){
//通过键盘输入获取需要向服务端发送的消息
String str = scanner.nextLine();
//将消息发送到服务端
pw.println(str);
pw.flush();
if("exit".equals(str)){
break;
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
3.3 TCP双向通信
3.3.1 创建服务端
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TwoWaySocketServer {
public static void main(String[] args) {
System.out.println("服务器启动,监听8888端口……");
try(ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
//字符输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//字符输出流
PrintWriter printWriter = new PrintWriter(socket.getOutputStream())
) {
//键盘输入
Scanner scanner = new Scanner(System.in);
while (true){
//先读取客户端的消息
String s = bufferedReader.readLine();
System.out.println("客户端信息:"+s);
//向客户端发送消息
String s1 = scanner.nextLine();
printWriter.write(s1);
//刷新
printWriter.flush();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
3.3.2 创建客户端
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
//客户端
public class TwoWaySocketClient {
public static void main(String[] args) {
try(//创建客户端对象
Socket socket = new Socket("127.0.0.1",8888);
//字符输出流
PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
//字符输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Scanner scanner = new Scanner(System.in);
) {
//键盘输入
System.out.println("(客户端)输入数据:");
while (true){
//发送数据
String s = scanner.nextLine();
printWriter.println(s);
printWriter.write(s);
printWriter.flush();
//接受数据
System.out.println("服务端消息:"+ bufferedReader.readLine());
}
}catch (Exception e){
e.printStackTrace();
}
}
}
程序小bug:只能先从客户端开始说,然后服务端才能说。只能一个人一句话。问题解决见3.4
3.4 点对点聊天应用
3.4.1 创建服务端
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
//发送消息的线程
class Send extends Thread{
private Socket socket; //与客户端对应的socket
public Send(Socket socket){
this.socket = socket;
}
//发送消息
public void sendMsg(){
try( //创建键盘输入对象
Scanner scanner = new Scanner(System.in);
//创建向客户端发送消息的输出流对象
PrintWriter printWriter = new PrintWriter(this.socket.getOutputStream())
) {
while (true){
String msg = scanner.nextLine();
printWriter.println(msg);
printWriter.flush();
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
//调用发送消息方法
sendMsg();
}
}
//接受线程
class Accept extends Thread{
private Socket socket; //与客户端对应的socket
public Accept(Socket socket){
this.socket = socket;
}
//接受消息
public void acceptMsg(){
try(
//字符输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()))
) {
while (true){
System.out.println("客户端说:"+bufferedReader.readLine());
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
acceptMsg();
}
}
//主线程,启动服务端
public class ChatSocketServer {
public static void main(String[] args) {
try(ServerSocket serverSocket = new ServerSocket(8888))
{
//监听客户端
System.out.println("服务端启动,正在监听客户端……");
Socket socket = serverSocket.accept();
System.out.println("连接成功!^_^");
new Send(socket).start(); //启动发送线程
new Accept(socket).start(); //启动接受线程
}catch (Exception e){
e.printStackTrace();
}
}
}
注意:主线程的作用就是去启动接受和发送线程,一旦主线程任务结束,主线程就死亡。主线程一旦死亡,try-with-resource就会将try括号中能关闭的流全部关闭,所以不能将Socket监听写在小括号内。可以写在小括号外面。
3.4.2 创建客户端
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
//接受消息线程
class ClientAccept extends Thread{
private Socket socket;
public ClientAccept(Socket socket){
this.socket = socket;
}
public void acceptMsg(){
try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(this.socket.getInputStream())))
{
while (true){
System.out.println("服务端说:"+bufferedReader.readLine());
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
acceptMsg();
}
}
//发送消息线程
class ClientSend extends Thread{
private Socket socket;
public ClientSend(Socket socket){
this.socket = socket;
}
public void sendMsg(){
try( //创建键盘输入对象
Scanner scanner = new Scanner(System.in);
//创建向客户端发送消息的输出流对象
PrintWriter printWriter = new PrintWriter(this.socket.getOutputStream())
) {
while (true){
String msg = scanner.nextLine();
printWriter.println(msg);
printWriter.flush();
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
sendMsg();
}
}
//主线程
public class ChatSocketClient {
public static void main(String[] args) throws IOException {
try {
Socket socket = new Socket("127.0.0.1",8888);
new ClientSend(socket).start(); //启动线程
new ClientAccept(socket).start();
}catch (Exception e){
e.printStackTrace();
}
}
}
这样就实现了客户端和服务器之间可以连续发送消息,而且消息的数量没有限制。
3.4.3 优化点对点
客户端和服务端的区别只是在连接的时候区分,当客户端和服务器连接成功之后没有区别了。
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
//发送线程
class TCPSend extends Thread{
private Socket socket;
private Scanner scanner;
public TCPSend(Socket socket,Scanner scanner){
this.scanner = scanner;
this.socket = socket;
}
public void send(){
try(PrintWriter printWriter = new PrintWriter(socket.getOutputStream()))
{
while (true){
String s = scanner.nextLine();
printWriter.println(s);
printWriter.flush();
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
send();
}
}
//接受线程
class TCPAccept extends Thread{
private Socket socket;
public TCPAccept(Socket socket){
this.socket = socket;
}
public void accept(){
try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(this.socket.getInputStream())))
{
while (true){
System.out.println(bufferedReader.readLine());
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
accept();
}
}
public class GoodTCP {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
try {
//键盘输入,根据键盘输入决定启动的是客户端还是服务端
System.out.println("输入'服务端'启动服务端;输入’客户端‘启动客户端");
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
if ("服务端".equals(s)){
System.out.println("正在启动服务器,请稍后O(∩_∩)O");
serverSocket = new ServerSocket(8888);
System.out.println("正在监听8888端口");
socket = serverSocket.accept();
}
if ("客户端".equals(s)){
socket = new Socket("127.0.0.1",8888);
System.out.println("客户端启动成功!O(∩_∩)O");
}
//启动线程
new TCPSend(socket,scanner).start();
new TCPAccept(socket).start();
}catch (Exception e){
e.printStackTrace();
}finally {
if (serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3.5 一对多应用设计
点对点聊天的缺点:因为客户端和服务端之间的连接是在主线程中,主线程的作用是启动接受和发送消息的线程,一旦主线程任务完成,accpet方法也结束了,那么服务端就无法再接受其他客户端的连接了。此时,只要将accpet单独拿出来,使服务端监听到每个与之连接的客户端,并返回对应的Socket对象即可。但是这样,客户端与客户端之间是隔离的,不能相互通信。
3.5.1 一对多应答型服务端
应答型客户端指的是:一个服务端和多个客户端连接,客户端向服务端发送数据时服务端再将数据返回给客户端。
//主线程
public class EchoServer {
public static void main(String[] args) {
try(ServerSocket serverSocket = new ServerSocket(8888)) {
while (true){
Socket socket = serverSocket.accept();//不能死
new Msg(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
3.5.2 一对多聊天应用实现
聊天服务器就是多个客户端和服务端连接,当一个客户端向服务端发送数据的时候,服务端会将这些数据发送给其他和服务端相连的客户端。
服务器的线程设计
实现思路,服务端的接受线程负责接受客户端的消息,接受到消息后将消息写入到公共数据区,与此同时唤醒所有客户端的发送线程将缓冲区中的数据发送给客户端。然后发送线程又处于阻塞状态。
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
//接受客户端消息的线程
class ChatReceive extends Thread{
private Socket socket;//拿到客户端对应的socket对象
public ChatReceive(Socket socket){
this.socket = socket;
}
public void receiveMsg(){
try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())))
{
while (true){
String s = bufferedReader.readLine();//读消息不受锁影响
synchronized ("aaa"){
//将读取的数据写入公共数据区
ChatRoomServer.buf = "["+socket.getInetAddress()+"]"+s;
//唤醒所有发送消息的线程
"aaa".notifyAll();
}
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
receiveMsg();
}
}
//向客户端发送消息的线程
class ChatSend extends Thread{
private Socket socket;//拿到客户端对应的socket对象
public ChatSend(Socket socket){
this.socket = socket;
}
public void sendMsg(){
try(PrintWriter printWriter = new PrintWriter(socket.getOutputStream());)
{
while (true){
//加锁,让线程实现同步(互斥)
synchronized ("aaa"){
//让发送消息的线程处于等待状态,只有服务端接受到数据后才能将数据发送出去。
"aaa".wait(); //表示拥有“aaa”对象锁的线程处于阻塞状态。
printWriter.println(ChatRoomServer.buf);
printWriter.flush();
}
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
sendMsg();
}
}
//主线程,专门负责客户端和服务端之间的连接
public class ChatRoomServer {
public static String buf; //公共数据区
public static void main(String[] args) {
System.out.println("服务端启动");
try(ServerSocket serverSocket = new ServerSocket(8888)) {
while (true){
Socket socket = serverSocket.accept();//不能死
System.out.println("连接到:"+socket.getInetAddress());
new ChatReceive(socket).start(); //启动线程
new ChatSend(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
package cn.it.bz.Socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
//接受消息
class ClientChatReceive extends Thread{
private Socket socket;
public ClientChatReceive(Socket socket) {
this.socket = socket;
}
public void accept(){
try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(this.socket.getInputStream())))
{
while (true){
System.out.println( bufferedReader.readLine());
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
accept();
}
}
//发送消息
class ClientChatSend extends Thread{
private Socket socket;
public ClientChatSend(Socket socket) {
this.socket = socket;
}
public void send(){
try(PrintWriter printWriter = new PrintWriter(socket.getOutputStream()))
{
System.out.println("请输入消息:");
Scanner scanner = new Scanner(System.in);
while (true){
String s = scanner.nextLine();
printWriter.println(s);
printWriter.flush();
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
send();
}
}
//聊天室客户端
public class ChatRoomClient {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1",8888);
new ClientChatSend(socket).start();
new ClientChatReceive(socket).start();
}catch (Exception e){
e.printStackTrace();
}
}
}
四、UDP通信
4.1 UDP实现原理
UDP协议与之前讲到的TCP协议不同,是面向无连接的,双方不需要建立连接便可通信。UDP通信所发送的数据需要进行封包操作(使用DatagramPacket类),然后才能接收或发送(使用DatagramSocket类)。虽然不可靠,但是效率高,视频会议网络聊天一般使用UDP协议。
DatagramPacket:数据容器(封包)的作用
此类表示数据报包。 数据报包用来实现封包的功能。
方法名 | 使用说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造数据报包,用来接收长度为 length 的数据包 |
DatagramPacket(byte[] buf, int length, InetAddress address, int port) | 构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号 |
getAddress() | 获取发送或接收方计算机的IP地址,此数据报将要发往该机器或者是从该机器接收到的 |
getData() | 获取发送或接收的数据 |
setData(byte[] buf) | 设置发送的数据 |
DatagramSocket:用于发送或接收数据报包
当服务器要向客户端发送数据时,需要在服务器端产生一个DatagramSocket对象,在客户端产生一个DatagramSocket对象。服务器端的DatagramSocket将DatagramPacket发送到网络上,然后被客户端的DatagramSocket接收。
DatagramSocket有两种常用的构造函数。一种是无需任何参数的,常用于客户端;另一种需要指定端口,常用于服务器端。如下所示:
- DatagramSocket() :构造数据报套接字并将其绑定到本地主机上任何可用的端口。
- DatagramSocket(int port) :创建数据报套接字并将其绑定到本地主机上的指定端口。
方法名 | 使用说明 |
---|---|
send(DatagramPacket p) | 从此套接字发送数据报包 |
receive(DatagramPacket p) | 从此套接字接收数据报包 |
close() | 关闭此数据报套接字 |
4.2 UDP通信编程基本步骤
- 创建客户端的DatagramSocket,创建时,定义客户端的监听端口。
- 创建服务器端的DatagramSocket,创建时,定义服务器端的监听端口。
- 在服务器端定义DatagramPacket对象,封装待发送的数据包。
- 客户端将数据报包发送出去。
- 服务器端接收数据报包。
4.3 实现UDP通信
4.3.1 创建服务端
package cn.it.bz.Socket;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServer {
public static void main(String[] args) {
//创建基于UDP协议的DatagramSocket对象。
//表示服务端要监听该电脑的8888端口。
try(DatagramSocket datagramSocket = new DatagramSocket(8888);)
{
//创建数据缓冲区
byte[] bytes = new byte[1024];
//创建数据包对象(就相当于是放东西的袋子)
DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length);
//等待接受客户端发送数据(服务端拿着袋子准备装数据)
datagramSocket.receive(datagramPacket);
//取出袋子中的数据
byte[] data = datagramPacket.getData();
//将字节数据转换成字符串类型【offset表示从数组哪个位置开始,datagramPacket.getLength()表示袋子中数据包的长度】
String s = new String(data,0,datagramPacket.getLength());
System.out.println("客户端发送的数据是:"+s);
}catch (Exception e){
e.printStackTrace();
}
}
}
4.3.2 创建客户端
package cn.it.bz.Socket;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
public class UDPClient {
public static void main(String[] args) {
//该端口为UDP协议发送数据指定端口。
// 如果服务端和客户端在一个设备上,那么客户端指定的端口和服务端监听的端口不能相同,否则会发生端口抢占。
try( DatagramSocket datagramSocket = new DatagramSocket(9999);) {
//数据需要转换为字节数组类型
byte[] bytes = "Java".getBytes();
//创建数据报包对象.后两个参数指定数据发送到哪个服务端以及服务端接受消息的端口
DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length,new InetSocketAddress("127.0.0.1",8888));
//向服务端发送数据
datagramSocket.send(datagramPacket);
}catch (Exception e){
e.printStackTrace();
}
}
}
4.4 基于UDP协议传输基本数据类型
4.4.1 创建服务端
package cn.it.bz.Socket;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class BasicTypeUDPServer {
public static void main(String[] args) {
try(DatagramSocket datagramSocket = new DatagramSocket(8888);)
{
//字节数组
byte[] bytes = new byte[1024];
//存放数据的袋子
DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length);
//等待接受客户端数据
datagramSocket.receive(datagramPacket);
//从袋子取数据
byte[] data = datagramPacket.getData();
//通过DataInputStream对象读取基本数据类型
//ByteArrayInputStream(bytes)可以直接从内存读取字节
try(DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(bytes))) {
System.out.println("客户端发送的数据:"+dataInputStream.readLong());
}
}catch (Exception e){
e.printStackTrace();
}
}
}
4.4.2 创建客户端
package cn.it.bz.Socket;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
public class BasicTypeUDPClient {
public static void main(String[] args) {
long data = 1000000L;
try(DatagramSocket datagramSocket = new DatagramSocket(9999);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
)
{
//将基本数据类型转换为数组
dataOutputStream.writeLong(data);
//将基本数据类型转换为数组
byte[] bytes = byteArrayOutputStream.toByteArray();
//数据包装
DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length,new InetSocketAddress("127.0.0.1",8888));
//数据发送
datagramSocket.send(datagramPacket);
}catch (Exception e){
e.printStackTrace();
}
}
}
4.5 基于UDP协议传递自定义对象
4.5.1 创建对象
package cn.it.bz.Socket;
import java.io.Serializable;
//使用流操作对象必须要实现序列化接口
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
4.5.2 创建服务端
package cn.it.bz.Socket;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class ObjectTypeServer {
public static void main(String[] args) {
try(DatagramSocket datagramSocket = new DatagramSocket(8888))
{
byte[] bytes = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length);
datagramSocket.receive(datagramPacket);
//接受数据
byte[] data = datagramPacket.getData();
//将对象转换为字节数组
try(ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(data)))
{
Person person = (Person) objectInputStream.readObject();
System.out.println(person);
}catch (Exception e){
e.printStackTrace();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
4.5.3 创建客户端
package cn.it.bz.Socket;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
public class ObjectTypeClient {
public static void main(String[] args) {
try(DatagramSocket datagramSocket = new DatagramSocket(9999);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//ByteArrayOutputStream将内存的数据读取到字节数组中,将对象转换为字节数组
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream))
{
//创建自定义对象
Person person = new Person("zhangsan",13);
objectOutputStream.writeObject(person);
byte[] bytes = byteArrayOutputStream.toByteArray();
//数据包装
DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length,new InetSocketAddress("127.0.0.1",8888));
//发送数据
datagramSocket.send(datagramPacket);
}catch (Exception e){
e.printStackTrace();
}
}
}