【JavaSE 网络编程】
一、对象的序列化
对象的序列化的意义:对象变成一个文件,在网络中传递,可以让任何一个人使用这个对象
1. ObjectOutputStream 对象的序列化
- 构造方法
ObjectOutputStream(OutputStream out)
传递字节输出流 void writeObject(Object o)
写入对象
JDK 中要求被序列化的对象必须实现 Serializable 接口
public interface Serializable
:类通过实现 java.io.Serializable 接口以启用其序列化功能,未实现此接口的类将无法使其任何状态序列化或者反序列化。
首先创建一个 Person 类:
import java.io.Serializable;
// 实现接口,以启用序列化
public class Person implements Serializable {
// 这个接口其中没有方法,是个空壳子,就是一个标识
private String name;
private int age;
// 序列号需要定义:
static final long serialVersionUID = 42L;
public Person() {}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
/**
* 对象的序列化
*/
public static void writeObj()throws IOException {
// 创建字节输出流,绑定文件
FileOutputStream fos = new FileOutputStream("day20_2/person.txt");
// 对象的序列化流创建
ObjectOutputStream oos = new ObjectOutputStream(fos);
// oos 流,写入对象
Person person = new Person("张三",20);
oos.writeObject(person); //写入对象
oos.close(); // 流对象的释放
}
2. ObjectInputStream 对象的反序列化
- 构造方法
ObjectInputStream (InputStream out)
传递字节输入流 Object readObject()
读取对象
/**
* 对象的反序列化
* ois.readObject(); 有一个特别的异常叫做:类找不到异常 ClassNotFoundException
* 抛出 ClassNotFoundException 异常的原因:没有 class 文件,抛出类找不到异常
*/
public static void readObj()throws Exception{
// 创建字节输入流,读取数据源文件,源文件必须是序列化后的文件
FileInputStream fis = new FileInputStream("day20_2/person.txt");
// 创建反序列化对象
ObjectInputStream ois = new ObjectInputStream(fis);
// 读取对象
Object object = ois.readObject(); // 必须进行强制类型转换
System.out.println(object);
ois.close();
}
注意:
- 静态不能进行序列化
- 如果有
private transient int age;
transient 的作用就是阻止变量的值,被序列化到文件 - 序列号需要定义:
static final long serialVersionUID = 42L;
前面的限定词不可以改变,但是 “42L” 这个值可以改变,序列号用作防止数据篡改,篡改后无法进行反序列化
二、软件结构
- C/S结构:全称为 Client/Server 结构,是指客户端和服务器结构。常见程序有QQ、微信、红蜘蛛、飞秋等软件。
- B/S结构:全称为 Browser/Server 结构,是指浏览器和服务器结构。常见浏览器有IE、谷歌、火狐等。
两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程:就是在一定的协议下,实现两台计算机的通信的程序。
1.网络通信协议
(1)TCP/IP 协议参考模型
网络通信协议:通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。
- TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol ),是 Internet 最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。
上图中,OSI 参考模型:模型过于理想化,未能在因特网上进行广泛推广。TCP/IP 参考模型(或 TCP/IP 协议):事实上的国际标准。
- TCP/IP 协议中的四层分别是应用层、传输层、网络层和链路层,每层分别负责不同的通信功能。
链路层:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。 - 网络层:网络层是整个 TCP/IP 协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。而 IP 协议是一种非常重要的协议。IP(internet protocal)又称为互联网协议。IP 的责任就是把数据从源传送到目的地。它在源地址和目的地址之间传送一种称之为数据包的东西,它还提供对数据大小的重新组装功能,以适应不同网络对包大小的要求。
- 传输层:主要使网络程序进行通信,在进行网络通信时,可以采用 TCP 协议,也可以采用 UDP 协议。TCP(Transmission Control Protocol)协议,即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。UDP (User Datagram Protocol,用户数据报协议):是一个无连接的传输层协议、提供面向事务的简单不可靠的信息传送服务。
- 应用层:主要负责应用程序的协议,例如 HTTP 协议、FTP 协议等。
而通常我们说的 TCP/IP 协议,其实是指 TCP/IP 协议族,因为该协议家族的两个最核心协议:TCP(传输控制协议)和 IP(网际协议),为该家族中最早通过的标准,所以简称为 TCP/IP 协议。
(2)TCP 与 UDP 协议
通信的协议还是比较复杂的,
java.net
包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。
java.net
包中提供了两种常见的网络协议的支持:
① UDP:用户数据报协议(User Datagram Protocol)
-
非面向连的,不可靠的:UDP 是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。
-
简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
-
由于使用 UDP 协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用 UDP 协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
-
但是在使用 UDP 协议传送数据时,由于 UDP 的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用 UDP 协议。
-
-
大小限制的:数据被限制在 64kb 以内,超出这个范围就不能发送了。
-
数据报(Datagram):网络传输的基本单位
② TCP:传输控制协议 (Transmission Control Protocol)
-
面向连接的,可靠的:TCP 协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。是一种面向连接的、可靠的、基于字节流的传输层的通信协议,可以连续传输大量的数据。类似于打电话的效果。
-
这是因为它为当一台计算机需要与另一台远程计算机连接时,TCP 协议会采用“三次握手”方式让它们建立一个连接,用于发送和接收数据的虚拟链路。数据传输完毕 TCP 协议会采用“四次挥手”方式断开连接。
-
TCP 协议负责收集这些信息包,并将其按适当的次序放好传送,在接收端收到后再将其正确的还原。TCP 协议保证了数据包在传送中准确无误。TCP 协议使用重发机制,当一个通信实体发送一个消息给另一个通信实体后,需要收到另一个通信实体确认信息,如果没有收到另一个通信实体确认信息,则会再次重复刚才发送的消息。
-
Ⅰ、三次握手:TCP 协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
- 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
- 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
- 第三次握手,客户端再次向服务器端发送确认信息,确认连接。
Ⅱ、四次挥手:TCP 协议中,在发送数据结束后,释放连接时需要经过四次挥手。
- 第一次挥手:客户端向服务器端提出结束连接,让服务器做最后的准备工作。此时,客户端处于半关闭状态,即表示不再向服务器发送数据了,但是还可以接受数据。
- 第二次挥手:服务器接收到客户端释放连接的请求后,会将最后的数据发给客户端。并告知上层的应用进程不再接收数据。
- 第三次挥手:服务器发送完数据后,会给客户端发送一个释放连接的报文。那么客户端接收后就知道可以正式释放连接了。
- 第四次挥手:客户端接收到服务器最后的释放连接报文后,要回复一个彻底断开的报文。这样服务器收到后才会彻底释放连接。这里客户端,发送完最后的报文后,会等待 2MSL,因为有可能服务器没有收到最后的报文,那么服务器迟迟没收到,就会再次给客户端发送释放连接的报文,此时客户端在等待时间范围内接收到,会重新发送最后的报文,并重新计时。如果等待 2MSL 后,没有收到,那么彻底断开。
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP 协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。
三、网络编程三要素
1.协议
协议:计算机网络通信必须遵守的规则。
2. IP 地址
IP地址:指互联网协议地址(Internet Protocol Address),俗称 IP。IP 地址用来给一个网络中的计算机设备做唯一的编号。假如我们把 “个人电脑” 比作 “一台电话” 的话,那么 “IP 地址” 就相当于“电话号码”。
(1)IP 地址分类方式一:
-
IPv4 :是一个32位的二进制数,通常被分为4个字节,表示成
a.b.c.d
的形式,例如192.168.65.100
。其中a、b、c、d都是 0~255 之间的十进制整数,那么最多可以表示42亿个。 -
IPv6 :由于互联网的蓬勃发展,IP 地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。
为了扩大地址空间,拟通过 IPv6 重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成
ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
(2)IP 地址分类方式二:
①公网地址(万维网使用) 和 ②私有地址(局域网使用)。
192.168. 开头的就是私有址址,范围即为 192.168.0.0–192.168.255.255,专门为组织机构内部使用
(3)常用命令:
- 查看本机IP地址,在控制台输入:
ipconfig
- 检查网络是否连通,在控制台输入:
ping 空格 IP地址
ping 220.181.57.216
(4)特殊的IP地址:
- 本地回环地址(hostAddress):
127.0.0.1
- 主机名(hostName):
localhost
(5)域名:
因为IP地址数字不便于记忆,因此出现了域名,域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。 ------- 域名解析
(6)获取本机 IP 及远程本机 IP 的相关操作:
import java.net.InetAddress;
import java.net.UnknownHostException; // 使用 IOException 也可以
/**
* IP 地址也是对象,java.net.InetAddress 类
*/
public class InetAddressTest {
public static void main(String[] args) throws UnknownHostException { // 使用 IOException 也可以
ipMethod01();
ipMethod02();
}
/**
* InetAddress 类获取本机 IP
* 静态方法:getLocalhost() 获取本地主机
* 返回的是本类的对象
*/
public static void ipMethod01() throws UnknownHostException { // 使用 IOException 也可以
InetAddress inetAddress=InetAddress.getLocalHost();
// inetAddress 调用方法 getHostAddress(); 返回字符串,本机的 IP 地址
String ip=inetAddress.getHostAddress();
System.out.println("ip = " + ip);
// inetAddress 调用方法 getHostName(); 返回字符串,主机名
String name=inetAddress.getHostName();
System.out.println("name = " + name);
}
/**
* InetAddress 类获取远程本机 IP
* 静态方法 getByName(远程主机的名称);
*/
public static void ipMethod02() throws UnknownHostException { // 使用 IOException 也可以
InetAddress inetAddress=InetAddress.getByName("Hello"); // 输入已知并且是联网的主机名
String ip=inetAddress.getHostAddress();
String hostName=inetAddress.getHostName();
System.out.println("ip = " + ip);
System.out.println("hostName = " + hostName);
}
}
3.端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)
解释:两个设备都拥有自己的操作系统,并且具有自己唯一的 IP 地址,而此时操作时同就相当于是旅店,Wechat、QQ 等软件就相当于是住客,于是就拥有了自己的房间号也就是端口号,当进行信息传递时,原始传递信息数据需要附带着接收者的 IP 地址以及软件的端口号进行传递,所以端口号是用来区分软件在操作系统中的位置,操作系统中一共有65535个端口号,每个软件都会占用一个或者两个端口号。(这种“软件”必须是网络软件,如果无需联网就可运行使用的软件无需端口号,并且网络软件只有在联网使用是才会占用端口号,否则不会使用)
- 端口号:用两个字节表示的整数,它的取值范围是 0~65535。
- 公认端口:0~1023。被预先定义的服务通信占用,如:HTTP(80),FTP(21),Telnet(23)
- 注册端口:1024~49151。分配给用户进程或应用程序。如:Tomcat(8080),MySQL(3306),Oracle(1521)。
- 动态/ 私有端口:49152~65535。
如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。
利用 协议
+IP地址
+端口号
三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。
四、 InetAddress 类
InetAddress 类主要表示 IP 地址,两个子类:Inet4Address、Inet6Address。
- Internet 上的主机有两种方式表示地址:
- 域名(hostName):www.xxxxxxx.com
- IP 地址(hostAddress):202.108.35.210
- lInetAddress 类没有提供公共的构造器,而是提供 了 如下几个 静态方法来获取 InetAddress 实例
public static InetAddress getLocalHost()
public static InetAddress getByName(String host)
public static InetAddress getByAddress(byte[] addr)
- InetAddress 提供了如下几个常用的方法
public String getHostAddress()
:返回 IP 地址字符串(以文本表现形式)。public String getHostName()
:获取此 IP 地址的主机名
import java.net.InetAddress;
import java.net.UnknownHostException;
import org.junit.Test;
public class TestInetAddress {
@Test
public void test01() throws UnknownHostException{
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost);
}
@Test
public void test02()throws UnknownHostException{
InetAddress address = InetAddress.getByName("www.xxxxxxx.com");
System.out.println(address);
}
@Test
public void test03()throws UnknownHostException{
// byte[] addr = {112,54,108,98};
byte[] addr = {(byte)192,(byte)168,24,56};
InetAddress address = InetAddress.getByAddress(addr);
System.out.println(address);
}
}
五、Socket
通信的两端都要有 Socket(也可以叫“套接字”),是两台机器间通信的端点。网络通信其实就是 Socket 间的通信。
在 TCP 协议下,一旦一个类经过 new Socket()
操作就成为了客户端,而另一个类一旦经过 new ServerSocket()
操作就成为了服务器,(服务器不会主动连接客户端,需要客户端主动连接服务器),客户端和服务器连接的通路中,自动创建 IO 流对象,所以网络编程主要就是使用 IO 技术(自己创建的 IO 流没有用)网络中全部都是字节流,没有字符流
Socket 可以分为:
- 流套接字(stream socket):使用 TCP 提供可依赖的字节流服务
- ServerSocket :此类实现 TCP 服务器套接字。服务器套接字等待请求通过网络传入。
- Socket :此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
- 数据报套接字(datagram socket):使用 UDP 提供“尽力而为”的数据报服务
- DatagramSocket:此类表示用来发送和接收 UDP 数据报包的套接字。
六、 TCP 网络编程
1. 通信模型
Java 语言的基于套接字 TCP 编程分为服务端编程和客户端编程,其通信模型如图所示:
2.开发步骤
(1)服务器端
服务器 程序的工作过程包含以下四个基本的 步骤:
- 调用
ServerSocket(int port)
:创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。 - 调用
accept()
:监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。 - 调用 该Socket 类对象的
getOutputStream()
和getInputStream ()
:获取输出流和输入流,开始网络数据的发送和接收。 - 关闭 Socket 对象:客户端访问结束,关闭通信套接字。
(2)客户端
客户端 Socket 的工作过程包含以下四个基本的步骤 :
- 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
- 打开连接到 Socket 的输入/ 出流: 使用
getInputStream()
方法获得输入流,使用getOutputStream()
方法获得输出流,进行数据传输 - 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线路。
- 关闭 Socket :断开客户端到服务器的连接,释放线路
服务器端:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 实现 TCP 协议的服务器
* java.net.ServerSocket 类
* 其中有一个关键的方法:ServerSocket 类的方法 accept();
* 方法的返回值是 Socket 对象,返回了客户端的连接对象 Socket 对象带着客户端的信息(IP,端口,主机名)
* 它的作用是等待客户端连接,线程处于等待状态(否则 main 方法一旦结束,服务器关闭,就无法连接客户端)
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
// 创建服务器端的套接字对象,一定要指定端口号
ServerSocket serverSocket=new ServerSocket(8888);
// 调用方法 accept() 等待客户端的连接
Socket socket = serverSocket.accept();// 线程处于等待状态
System.out.println("socket = " + socket);
// 得到了socket = Socket[addr=/127.0.0.1,port=2772,localport=8888] ”2772“ 是客户端出操作系统时的端口号
// 通过客户端的连接对象 Socket ,获取字节输入流,读取客户端的数据
InputStream inputStream= socket.getInputStream();
byte[] bytes=new byte[6]; // 由于客户端中写入的”你好“为六个字节
// 读取客户端数据
int r = inputStream.read(bytes);
System.out.println(new String(bytes,0,r));
// 给客户端返回数据,使用字节输出流,将数据写回客户端
OutputStream out= socket.getOutputStream();
out.write("收到".getBytes());
// 释放资源,断开连接
socket.close();
// 释放操作系统端口号
serverSocket.close(); // 但是真正的服务器是一直开机运行状态
}
}
客户端:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* 实现 TCP 协议的客户端
* java.net.Socket 类
*/
public class TCPClient {
public static void main(String[] args) throws IOException {
// 创建 Socket 类对象,传递两个参数,服务器 IP 地址,服务器端口号
// 由于没有另一台机器所以自己作为自己的服务器
Socket socket=new Socket("127.0.0.1",8888); // 连接服务器
// 进行数据传输
// 只能通过连接对象 Socket 获取 IO 中的字节输出流
OutputStream out = socket.getOutputStream(); // 数据写入服务器
out.write("你好".getBytes()); // 已写进服务器
// 通过连接对象 Socket 获取字节输入流,读取服务器返回来的数据
InputStream inputStream= socket.getInputStream();
byte[] bytes=new byte[6]; // ”收到“为六个字节
int r = inputStream.read(bytes);
System.out.println(new String(bytes,0,r));
// 释放资源,断开连接即可
socket.close();
}
}
传递图片的案例:
服务器:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 实现文件上传的服务器
* 1. 建立服务器对象,打开端口号
* 2. accept(); 等待客户端连接
* 3. 字节输入流,读取客户发来的数组
* 4. 字节输出流,数组保存在自己的磁盘中
* 5. 向客户端返回写“上传成功”
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
// 1.建立服务器对象,打开端口号
ServerSocket serverSocket=new ServerSocket(8888);
// 2.accept(); 等待客户端连接
Socket socket=serverSocket.accept();
// 3.字节输入流,读取客户端发来的数组
InputStream inputStream= socket.getInputStream();
byte[] bytes=new byte[1024*10];
int r=0;
// 4.字节输出流,数组保存在自己的磁盘中
FileOutputStream fos=new FileOutputStream("E:/server/1.jpg");
while ((r=inputStream.read(bytes))!=-1){
fos.write(bytes,0,r);
}
// 5.客户端返回写,上传成功
socket.getOutputStream().write("上传成功".getBytes());
// 6.释放资源
fos.close();
socket.close();
serverSocket.close();
}
}
客户端:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* 实现 TCP 文件上传的客户端
* 1. 连接服务器
* 2. 字节流,读取磁盘中的图片
* 3. 连接对象的字节输出流,读取到图片写到服务器
* 4. 读取服务器返回来的”上传成功“
*/
public class TCPClient {
public static void main(String[] args) throws IOException {
// 1.连接服务器
Socket socket=new Socket("127.0.0.1",8888);
// 2.字节流,读取磁盘中的图片
FileInputStream fis=new FileInputStream("c:/1.jpg");
// 字节数组缓冲
byte[] bytes=new byte[1024*10];
int r=0;
// 3.Socket 对象,获取字节输出流,数组写向服务器
OutputStream out = socket.getOutputStream();
while((r=fis.read(bytes))!=-1){
out.write(bytes,0,r);
}
// 没有添加内容,图片虽然会传递过去,但是客户端和服务器都不会停止陷入死循环
// **添加:在这里需要告诉服务器,数据没有了,不需要再等了
// socket 对象中的方法 shutdownOutput();
socket.shutdownOutput(); // 终止此字节输出流,终止之前,向服务器发生一个 TCP 的终止序列符号
// 4.读取服务器返回来的”上传成功“
InputStream inputStream= socket.getInputStream();
r=inputStream.read(bytes);
System.out.println("服务器返回:"+new String(bytes,0,r));
// 5.释放资源
fis.close();
socket.close();
}
}
上传案例中没有添加内容下,陷入死循环的原因:
- IO 中的
read()
方法具有线程阻塞的作用:从输入流中读取数据的下一个字节,返回 0~255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1 。在输入数据可用,检测到流末尾或者抛出异常前,此方法一直阻塞。
解决方法:
socket 对象中的方法 shutdownOutput()
,终止此字节输出流,终止之前,向服务器发生一个 TCP 的终止序列符号
优化服务器中的文件名覆盖问题:
解决方法:设定自定义文件名 = 公司域名+毫秒值+随机数 (UUID)
使得传递文件永不覆盖
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;
/**
* 实现文件上传的服务器
* 1. 建立服务器对象,打开端口号
* 2. accept(); 等待客户端连接
* 3. 字节输入流,读取客户发来的数组
* 4. 字节输出流,数组保存在自己的磁盘中
* 5. 向客户端返回写“上传成功”
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
// 1.建立服务器对象,打开端口号
ServerSocket serverSocket=new ServerSocket(8888);
// 2.accept(); 等待客户端连接
Socket socket=serverSocket.accept();
// 3.字节输入流,读取客户端发来的数组
InputStream inputStream= socket.getInputStream();
byte[] bytes=new byte[1024*10];
int r=0;
// 4.字节输出流,数组保存在自己的磁盘中
// 设定自定义文件名 = 公司域名+毫秒值+随机数 (UUID)
String filename="xxxxxxx"+System.currentTimeMillis()+new Random().nextInt(9999999)+"jpg";
// 创建 File 对象,字符串的父路径,字符串子路径
File file=new File("E:/server",filename);
FileOutputStream fos=new FileOutputStream(file);
while ((r=inputStream.read(bytes))!=-1){
fos.write(bytes,0,r);
}
// 5.客户端返回写,上传成功
socket.getOutputStream().write("上传成功".getBytes());
// 6.释放资源
fos.close();
socket.close();
serverSocket.close();
}
}
3. 相关 API
(1)第一部分:
① ServerSocket 类的构造方法:
ServerSocket(int port)
:创建绑定到特定端口的服务器套接字。
② ServerSocket 类的常用方法:
Socket accept()
:侦听并接受到此套接字的连接。
③ Socket 类的常用构造方法:
public Socket(InetAddress address,int port)
:创建一个流套接字并将其连接到指定 IP 地址的指定端口号。public Socket(String host,int port)
:创建一个流套接字并将其连接到指定主机上的指定端口号。
④ Socket 类的常用方法:
public InputStream getInputStream()
:返回此套接字的输入流,可以用于接收消息public OutputStream getOutputStream()
:返回此套接字的输出流,可以用于发送消息public InetAddress getInetAddress()
:此套接字连接到的远程 IP 地址;如果套接字是未连接的,则返回 null。public InetAddress getLocalAddress()
:获取套接字绑定的本地地址。public int getPort()
:此套接字连接到的远程端口号;如果尚未连接套接字,则返回 0。public int getLocalPort()
:返回此套接字绑定到的本地端口。如果尚未绑定套接字,则返回 -1。public void close()
:关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接或重新绑定)。需要创建新的套接字对象。 关闭此套接字也将会关闭该套接字的 InputStream 和 OutputStream。public void shutdownInput()
:如果在套接字上调用shutdownInput()
后从套接字输入流读取内容,则流将返回 EOF(文件结束符)。 即不能在从此套接字的输入流中接收任何数据。public void shutdownOutput()
:禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。 如果在套接字上调用shutdownOutput()
后写入套接字输出流,则该流将抛出 IOException。 即不能通过此套接字的输出流发送任何数据。
注意:先后调用 Socket 的 shutdownInput()
和 shutdownOutput()
方法,仅仅关闭了输入流和输出流,并不等于调用Socket的close()
方法。在通信结束后,仍然要调用 Scoket 的 close()
方法,因为只有该方法才会释放 Socket 占用的资源,比如占用的本地端口号等。
(2)第二部分:
UDP (User Datagram Protocol,用户数据报协议):是一个无连接的传输层协议、提供面向事务的简单不可靠的信息传送服务,类似于短信。
-
UDP 协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送,至于对方是否可以接收到这些数据内容,UDP 协议无法控制,因此说,UDP 协议是一种不可靠的协议。无连接的好处就是快,省内存空间和流量,因为维护连接需要创建大量的数据结构。UDP 会尽最大努力交付数据,但不保证可靠交付,没有 TCP 的确认机制、重传机制,如果因为网络原因没有传送到对端,UDP 也不会给应用层返回错误信息。
-
UDP 协议是面向数据报文的信息传送服务。UDP 在发送端没有缓冲区,对于应用层交付下来的报文在添加了首部之后就直接交付于 ip 层,不会进行合并,也不会进行拆分,而是一次交付一个完整的报文。比如我们要发送100个字节的报文,我们调用一次
send()
方法就会发送100字节,接收方也需要用receive()
方法一次性接收100字节,不能使用循环每次获取10个字节,获取十次这样的做法。 -
UDP 协议没有拥塞控制,所以当网络出现的拥塞不会导致主机发送数据的速率降低。虽然 UDP 的接收端有缓冲区,但是这个缓冲区只负责接收,并不会保证 UDP 报文的到达顺序是否和发送的顺序一致。因为网络传输的时候,由于网络拥塞的存在是很大的可能导致先发的报文比后发的报文晚到达。如果此时缓冲区满了,后面到达的报文将直接被丢弃。这个对实时应用来说很重要,比如:视频通话、直播等应用。
因此 UDP 适用于一次只传送少量数据、对可靠性要求不高的应用环境,数据报大小限制在 64K 以下。
(3)第三部分:
基于 UDP 协议的网络编程仍然需要在通信实例的两端各建立一个 Socket,但这两个 Socket 之间并没有虚拟链路,这两个 Socket 只是发送、接收数据报的对象,Java 提供了 DatagramSocket 对象作为基于 UDP 协议的 Socket ,使用 DatagramPacket 代表 DatagramSocket 发送、接收的数据报。
① DatagramSocket 类的常用方法:
public DatagramSocket(int port)
创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被绑定到通配符地址,IP 地址由内核来选择。public DatagramSocket(int port,InetAddress laddr)
创建数据报套接字,将其绑定到指定的本地地址。本地端口必须在 0 到 65535 之间(包括两者)。如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地址,IP 地址由内核选择。public void close()
关闭此数据报套接字。public void send(DatagramPacket p)
从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。public void receive(DatagramPacket p)
从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的长度长,该信息将被截短。
② DatagramPacket 类的常用方法:
public DatagramPacket(byte[] buf,int length)
构造 DatagramPacket,用来接收长度为 length 的数据包。 length 参数必须小于等于buf.length
。public DatagramPacket(byte[] buf,int length,InetAddress address,int port)
构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。length 参数必须小于等于buf.length
。public int getLength()
返回将要发送或接收到的数据的长度。
5.示例代码
(1)发送端:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
public class Send {
public static void main(String[] args)throws Exception {
// 1、建立发送端的 DatagramSocket
DatagramSocket ds = new DatagramSocket();
// 要发送的数据
ArrayList<String> all = new ArrayList<String>();
all.add("你好");
all.add("发送数据");
all.add("请接收");
all.add("回复");
// 接收方的 IP 地址
InetAddress ip = InetAddress.getByName("127.0.0.1");
// 接收方的监听端口号
int port = 9999;
// 发送多个数据报
for (int i = 0; i < all.size(); i++) {
// 2、建立数据包 DatagramPacket
byte[] data = all.get(i).getBytes();
DatagramPacket dp = new DatagramPacket(data, data.length, ip, port);
// 3、调用 Socket 的发送方法
ds.send(dp);
}
// 4、关闭 Socket
ds.close();
}
}
(2)接收端:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class Receive {
public static void main(String[] args) throws Exception {
// 1、建立接收端的 DatagramSocket,需要指定本端的监听端口号
DatagramSocket ds = new DatagramSocket(9999);
// 一直监听数据
while(true){
// 2、建立数据包 DatagramPacket
byte[] buffer = new byte[1024*64];
DatagramPacket dp = new DatagramPacket(buffer , buffer.length);
//3、调用 Socket 的接收方法
ds.receive(dp);
// 4、拆封数据
String str = new String(buffer,0,dp.getLength());
System.out.println(str);
}
}
}