文章目录
网络编程
一、网络编程入门
软件分为:系统软件和应用软件
1、应用软件结构
-
C/S结构:全称Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。
安装、运行、维护(升级,补丁)都是由客户端完成的,开发是由服务器端的程序员开发的。
-
B/S结构:全称Browser/Server结构,是指浏览器和服务器结构。
开发、运行、维护都是在服务器端完成,浏览器只负责浏览使用。
2、网络通信协议
- 网络通信协议:通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵循一定的规则。在计算机网络中,这些连接和通信的规则被称之为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。
- TCP/IP协议:传输控制协议/因特网互联协议(Transmission Control Protocol/Internet Protocol),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。
TCP/IP协议中的四层分为:应用层、传输层、网络层、链路层,每层分别负责不同的通信功能
链路层:链路层是用于定义物理传输通信,通常是对某些网络连接设备的驱动协议。如:光纤、网线提供的驱动。
网络层:网络层是整个TCP/IP协议的核心,他主要用于将传输的数据进行分组,将分组数据发送到目标计算机或网络。
运输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以使用UDP协议。
应用层:主要负责应用程序的协议。如:HTTP协议、FTP协议。
3、协议分类
通信的协议比较复杂,java.net
包中包含的类和接口,它们提供底层的通信细节。可直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信细节。
UDP
用户数据报协议(User Datagram Protocol)。UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。数据报是网络传输的基本单位。
由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输。在传输过程中偶尔会丢失一两个数据包,也不会产生多大的影响。
但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。
特点: 数据被限制在64kb以内,超出这个范围就不能发送。
TCP
传输控制协议(Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供两台计算机之间可靠无差错的数传输。
在TCP连接中必须要明确客户端与服务端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。
三次握手
TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠性
- 第一次握手,客户端向服务器发送连接请求,等待服务器的确认;
- 第二次握手,服务器向客户端回送一个响应,通知客户端收到了连接请求;
- 第三次握手,客户端再次向服务器发送确认信息,确认连接。
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证数据的安全,所以应用十分广泛。如:下载文件、浏览网页等。
四次挥手
也称四次握手
客户端关闭socket
第一次挥手:客户端发送FIN报文给服务端
第二次挥手:服务端接收到客户端的FIN报文,向客户端发送确认ACK报文
服务端关闭socket
第三次挥手:服务端发送FIN报文给客户端
第四次挥手:客户端接收到服务端的FIN报文,向服务端发送确认ACK报文
客户端服务端连接关闭完成
4、网络编程三要素
协议
计算机网络通信必须遵守的规则
IP地址
(Internet Protocol Address) IP地址用来给一个网络中的计算机设备做唯一的编号。
IP地址分类
- IPv4:是一个32位的二进制数,通常被分为4个字节,表示成
a.b.c.d
的形式,其中a、b、c、d都是0~255之间的十进制整数,最多可表示42亿个。 - IPv6:由于互联网的蓬勃发展,IP地址的需求量大,但网络资源有限,使得IP的分配紧张。为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节为一组,分成8组十六进制数,表示成
ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
。
常用命令
- 查看本机ip地址,控制台输入
ipconfig
- 检查网络是否连通
ping IP地址
Ping www.baidu.com
特殊IP地址
- 本机IP地址
127.0.0.1
、localhost
端口号
网络的通信,本质上是两个进程的通信。每台计算机都有很多的进程。如果说IP地址时标识网络中的设备,那么端口号就可以唯一标识设备中的进程。
-
端口号:用2个字节表示的整数,它的取值范围是0~65535 。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另一个服务或应用占用,会导致应用程序启动失败。
-
常见端口号:
- 80端口 网络端口
- 数据库 MySQL:3306 Oracle:1521
- Tomcat服务器 8080
二、TCP通信程序
TCP通信能实现计算机之间的数据交互,通信的两端,要严格区分客户端(Client)和服务端(Server)
在Java中,提供了两个类用于实现TCP通信程序:
- 客户端:
java.net.Socket
类表示。创建Socket
对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信 - 服务端:
java.net.ServerSocket
类表示。创建ServerSocket
对象,相当于开启一个服务,并等待客户端的连接
客户端和服务端进行逻辑连接,这个连接中包含一个IO对象,同过IO对象进行通信,因通信不仅仅是字符所以IO对象是字节流对象。
1、客户端与服务端的通信原理
注意:
服务端没有字节流对象,是通过获取客户端的Socket对象,来使用客户端的字节流对象,与客户端通信,这样可以准确的与多个客户端进行通信。
2、Socket 类
java.net.Socket
类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点(包含了IP地址和端口号的一种网络单位)。
常用构造方法
-
public Socket(String host,int port) throws UnknownHostException,IOException
创建流套接字并将其连接到指定主机上的指定端口号。如果指定的主机host
是null
,相当于指定回送接口的地址。回送接口地址(回送地址)是本机回送地址,只要用于网络软件测试以及本地机进程间的通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。
常用方法
public OutputStream getOutputStream() throws IOException
返回此套接字的输出字节流public IntputStream getIntputStream() throws IOException
返回此套接字的输入字节流public void close() throws IOException
关闭此套接字public void shutdownOutput() throws IOException
给网络输出流一个正常的终止符
public class TCPClient {
public static void main(String[] args) throws IOException {
// 创建一个客户端套接字对象,连接到主机的host地址、端口号
Socket socket = new Socket("127.0.0.1",8888);
// 获取套接字对象中的字节输出流
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream();
// 将数据通过套接字对象的字节输出流写出去
os.write("你好服务端,我是客户端".getBytes());
os.flush();
// 读取由服务端发来的信息
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes,0,len));
// 关闭套接字
socket.close();
}
}
3、ServerSocket 类
java.net.ServerSocket
这个类实现了服务器套接字。 服务器套接字等待通过网络进入的请求。 它根据该请求执行一些操作,然后可能将结果返回给请求者。
常用构造方法
public ServerSocket(int port) throws IOException
创建绑定到指定端口的服务器套接字。
常用方法
public Socket accept() throws IOException
监听并接收此套接字
public class TCPServer {
public static void main(String[] args) throws IOException {
// 创建一个服务端,指定端口号
ServerSocket serverSocket = new ServerSocket(8888);
// 获取请求的客户端的套接字对象
Socket socket = serverSocket.accept();
// 使用客户端的字节输入流,读取客户端发送的信息
InputStream is = socket.getInputStream();
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes,0,len));
// 使用客户端的字节输出流,向客户端发送信息
OutputStream os = socket.getOutputStream();
os.write("谢谢".getBytes());
os.flush();
// 关闭客户端对象,关闭服务端对象
socket.close();
serverSocket.close();
}
}
三、综合案例
1、文件上传案例
注意:
- 客户端、服务端和本地硬盘的读写,需要使用自己创建的字节流对象(本地流)
- 客户端和服务端之间进行读写,必须使用Socket中提供的字节流对象(网络流)
文件上传的原理就是文件的复制。
客户端:
public class TCPClient {
public static void main(String[] args) throws IOException {
// 创建一个客户端套接字,并指定要请求的ip地址和端口号
Socket socket = new Socket("127.0.0.1",8888);
// 获取套接字的网络输出流,为了向服务器上传文件
OutputStream os = socket.getOutputStream();
// 创建一个本地字节流,将本地文件读取进来,写到网络输出流中
FileInputStream fileInputStream = new FileInputStream("d:/1.jpg");
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fileInputStream.read(bytes)) != -1){
os.write(bytes,0,len); // 写入到网络输出流中,这里通过read()读取到的结束符无法写入到网络输出流中,因此服务端的read()方法进入阻塞状态,因为读取不到客户端发送的EOF结束符
}
// 因此要调用该方法向网络输出流写入正常终止符,但该方法调用之后,该流输出流将无法再次使用,单方面关闭流
socket.shutdownOutput();
// 创建一个网络输入流,读取从服务器过来的信息
InputStream is = socket.getInputStream();
byte[] bytesIs = new byte[1024];
int lenIs = 0;
while ((lenIs = is.read(bytesIs)) != -1){
System.out.println(new String(bytesIs,0,lenIs)); // 输出服务器传到客户端的信息
}
fileInputStream.close();
socket.close();
}
}
服务端:
public class TCPServer {
public static void main(String[] args) throws IOException {
// 创建一个服务端套接字,并指定端口号
ServerSocket serverSocket = new ServerSocket(8888);
// 接收上传的文件,存入到d:/upload文件夹中,通过判断文件夹是否存在,不存在就创建文件夹
File file = new File("d:/upload");
if(!file.exists()){
file.mkdirs();
}
// 创建一个本地的字节输出流,将上传过来的文件存入到对应的文件夹中
FileOutputStream fileOutputStream = new FileOutputStream(file+"/1.jpg");
// 获取客户端的套接字对象,为了获取客户端的输入字节流,用于接收上传的文件
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
byte[] byteIs = new byte[1024];
int lenIs = 0;
while ((lenIs = is.read(byteIs)) != -1){
fileOutputStream.write(byteIs,0,lenIs); // 通过本地输出流,写入到硬盘指定文件中
}
// 通过客户端套接字,给客户端发送成功消息
socket.getOutputStream().write("上传成功!".getBytes());
fileOutputStream.close();
socket.close();
serverSocket.close();
}
}
IO流read()方法遇到阻塞问题 重点
- 从本地硬盘向内存读取文件:
int read1 = 0;
byte[] bytes = new byte[1024];
while ((read1 = inputStream.read(bytes)) != -1){
// 该方法就是从字节数组索引为0开始到有效个数的长度截至,转为字符
System.out.println(new String(bytes,0, read1));
}
System.out.println("read1 "+read1);
输出:
read1 -1
- 网络编程中,读取本地硬盘文件向网络输出流写入
OutputStream os = socket.getOutputStream();
FileInputStream fileInputStream = new FileInputStream("d:/1.jpg");
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fileInputStream.read(bytes)) != -1){
os.write(bytes,0,len); // 写入到网络输出流中,这里通过read()读取到的结束符无法写入到网络输出流中,因此服务端的read()方法进入阻塞状态,因为读取不到客户端发送的EOF结束符
// System.out.println("len "+len);
}
// System.out.println("111"); 因为服务端read()方法处于阻塞状态无法执行
// 因此要调用该方法向网络输出流写入正常终止符,但该方法调用之后,该流将无法再次使用
socket.shutdownOutput();
System.out.println("len "+len);
输出:
len -1
以上两个方式,read()方法都读取到了文末,并都读取到EOF结束符,都会返回-1。第一个,打印流能够读取到read()方法读取到的结束符,因此打印流能够终止打印流的输出,都能够正常往下执行。第二个,网络编程中,因为客户端read()方法读取到了文件文末EOF结束符,但是向网络流中写入数据时,网络输出流只读取数据,客户端read()方法从本地文件中读取到的EOF结束符,不能通过网络输出流传输出去。因此,服务端通过read()方法读取网络输入流时,读取不到EOF结束符,从而进入到阻塞状态。客户端read()方法是返回了-1,跳出了循环,只是服务端read()方法处于阻塞状态,还一直处于读取状态,从而导致了,客户端和服务端的read()方法之后的代码都无法被执行。解决方法是,向服务端发送EOF结束符,告知服务端read()方法,已传输结束。
read()阻塞原因:客户端没有发送结束符,服务端一直阻塞等待读取。
-
read方法退出的条件:读到文件尾结束符EOF,返回-1
-
客户端的网络输出流不会发送文件尾和结束符,只发送指定的数据
网络字节输入输出流,客户端先从本地读取再向网络输出,读到-1则结束读取(但并不会把结束符和-1向网络输出流输出,所以服务端的网络输入流读取不到结束符)
本地文件尾有EOF,read读到后返回-1退出;因网络输入输出流读取不到结束符,这里案例客户端向服务端写数据,因无法将结束符写入到网络流中,服务端接收不到结束符,服务端会一直处于读取状态。从而服务端read()方法因读取不到EOF结束符,而进入阻塞,客户端服务端read()方法后面的代码不会被执行。
解决:客户端发送结束符
-
1 ) socket对象.shutdownOutput / socket对象.shutdownInput
套接字的shutdown方法是半关闭的 只在shutdown的这个流发送结束符EOF,服务端的read读到后会结束单个流 后面的代码可以继续正常执行并且不影响套接字的其他流对象。同样服务器端输出完毕之后也要shutdownoutput,否则客户端处于阻塞等待服务器端传数据的状态,但服务器都关闭了,自然连接异常断开,会报异常。
shutdown某个流后是不能再次使用的,除非客户端重新与服务器进行连接,重新生成新的流。
-
2) 自定义字符边界,通过两边的方法判断字符边界而判断是否结束
客户端:
/* * 输出流每次输出完毕后,给一个唯一的自定义字符作为文件边界 */ //向客户端写入可下载的文件目录 while ((len = fileInputStream.read(bytes)) != -1){ os.write(bytes,0,len); } //向客户端写入自定义字符 clientOps.write("我传完了".getBytes()); // 这样会将EOF结束符发送到网络输出流中
服务端:
/* * 输入流读取时每次判断是否本次从缓冲区拿到的有效字符结尾equals文件边界。 * 若是,代表本次已经读到了末尾,可以手动break当前循环读取的过程,退出read的阻塞。 */ String serverOutput=""; clientIps = client.getInputStream(); //获取所有文件并控制台输出 while((len = clientIps.read(b))!=-1){ serverOutput+=new String (b,0,len); //判断是否到达边界 if(serverOutput.substring(serverOutput.length()-4, serverOutput.length()).equals("我传完了")){ //将自定义边界去除掉 serverOutput=serverOutput.substring(0, serverOutput.length()-4); break; } } System.out.println(serverOutput);
2、文件上传案例 服务端优化
优化内容:
- 上传文件命名优化(防止重复,文件覆盖)
- 服务端一直处于监听状态(可接收多个客户端接入)
- 开启多线程处理来自客户端的上传服务(提高效率)
服务端:
public class TCPServer {
public static void main(String[] args) throws IOException {
// 创建一个服务端套接字,并指定端口号
ServerSocket serverSocket = new ServerSocket(8888);
// 接收上传的文件,存入到d:/upload文件夹中,通过判断文件夹是否存在,不存在就创建文件夹
File file = new File("d:/upload");
if(!file.exists()){
file.mkdirs();
}
// 开启线程池,线程池中有3个已有线程
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// while循环,让服务端一直处于监听状态
while (true){
threadPool.submit(()->{
// 在Runnable接口实现run方法中声明本地输出流,为了在finally中能够关闭该流
FileOutputStream fileOutputStream = null;
// try方法中 获取客户端的套接字对象,有异常会自动关闭socket,JDK7新特性
try(Socket socket = serverSocket.accept();){
// 为了使上传文件名字不重复,通过ip地址+毫秒+随机数命名
String fileName = "/iplocal" + System.currentTimeMillis() + new Random().nextInt(999999) + ".jpg";
// 创建一个本地的字节输出流,将上传过来的文件存入到对应的文件夹中
fileOutputStream = new FileOutputStream(file + fileName);
// 为了获取客户端的网络输入字节流,用于接收上传的文件,将文件输出到本地文件中
InputStream is = socket.getInputStream();
byte[] byteIs = new byte[1024];
int lenIs = 0;
while ((lenIs = is.read(byteIs)) != -1){ // 读取客户端发送的数据,会进入阻塞状态,因为结束符无法与数据一起传输过来,除非客户端发送结束符,以表结束
fileOutputStream.write(byteIs,0,lenIs); // 通过本地输出流,写入到硬盘指定文件中
}
// 通过客户端套接字,给客户端发送成功消息
socket.getOutputStream().write("上传成功!".getBytes());
}catch (IOException e){
System.out.println("Server IO异常");
}finally {
try {
fileOutputStream.close();
} catch (IOException e) {
System.out.println("Server IO关闭异常");
}
}
});
}
}
}
3、模拟B\S服务器
模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果
思路分析:
服务器:
public class TCPBSServer {
public static void main(String[] args) throws IOException {
// 创建一个服务端套接字,指定端口
ServerSocket serverSocket = new ServerSocket(8080);
// while循环处于一直监听状态
while (true) {
// 获取客户端套接字
Socket socket = serverSocket.accept();
// 创建线程,可以实现多请求处理 提高效率
new Thread(() -> {
FileInputStream fileInputStream = null;
try {
// 获取网络输入流
InputStream socketIS = socket.getInputStream();
// 通过缓冲流,可以获取请求头的第一行信息 通过该请求地址请求
// http://127.0.0.1:8080/web/index.html
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socketIS));
// 获取的是GET /web/index.html HTTP/1.1
String line = bufferedReader.readLine();
String[] s = line.split(" "); // 以空格分割,获取的是数组
String str = s[1]; // 获取的是/web/index.html
// 将从索引1开始的返回 web/index.html 这是需要读取的文件地址
String htmlPath = str.substring(1);
// 获取网络输出流和本地输入流
OutputStream socketOS = socket.getOutputStream();
fileInputStream = new FileInputStream(htmlPath);
// 向网络输出流中固定写入
socketOS.write("HTTP/1.1 200 OK\r\n".getBytes());
socketOS.write("Content-Type:text/html\r\n".getBytes());
// 必须要写入空行,否则浏览器不解析
socketOS.write("\r\n".getBytes());
// 将本地读取的html文件写入到网络输出流中,传给浏览器
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fileInputStream.read(bytes)) != -1) {
socketOS.write(bytes, 0, len);
socketOS.flush();
}
} catch (IOException e) {
System.out.println("IO异常");
}finally {
try {
fileInputStream.close();
socket.close();
} catch (IOException e) {
System.out.println("关闭 异常");
}
}
}).start();
}
}
}
此时可通过浏览器访问http://127.0.0.1:8080/web/index.html,获取到网页了(一定要注意访问地址正不正确)