软件分为:系统软件和应用软件
C/S结构:全称Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。
安装、运行、维护(升级,补丁)都是由客户端完成的,开发是由服务器端的程序员开发的。
B/S结构:全称Browser/Server结构,是指浏览器和服务器结构。
开发、运行、维护都是在服务器端完成,浏览器只负责浏览使用。
TCP/IP协议中的四层分为:应用层、传输层、网络层、链路层,每层分别负责不同的通信功能
链路层:链路层是用于定义物理传输通信,通常是对某些网络连接设备的驱动协议。如:光纤、网线提供的驱动。
网络层:网络层是整个TCP/IP协议的核心,他主要用于将传输的数据进行分组,将分组数据发送到目标计算机或网络。
运输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以使用UDP协议。
应用层:主要负责应用程序的协议。如:HTTP协议、FTP协议。
通信的协议比较复杂,java.net
包中包含的类和接口,它们提供底层的通信细节。可直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信细节。
用户数据报协议(User Datagram Protocol)。UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。数据报是网络传输的基本单位。
由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输。在传输过程中偶尔会丢失一两个数据包,也不会产生多大的影响。
但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。
特点: 数据被限制在64kb以内,超出这个范围就不能发送。
传输控制协议(Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供两台计算机之间可靠无差错的数传输。
在TCP连接中必须要明确客户端与服务端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。
TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠性
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证数据的安全,所以应用十分广泛。如:下载文件、浏览网页等。
也称四次握手
客户端关闭socket
第一次挥手:客户端发送FIN报文给服务端
第二次挥手:服务端接收到客户端的FIN报文,向客户端发送确认ACK报文
服务端关闭socket
第三次挥手:服务端发送FIN报文给客户端
第四次挥手:客户端接收到服务端的FIN报文,向服务端发送确认ACK报文
客户端服务端连接关闭完成
计算机网络通信必须遵守的规则
(Internet Protocol Address) IP地址用来给一个网络中的计算机设备做唯一的编号。
IP地址分类
a.b.c.d
的形式,其中a、b、c、d都是0~255之间的十进制整数,最多可表示42亿个。ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
。常用命令
ipconfig
ping IP地址
Ping www.baidu.com
特殊IP地址
127.0.0.1
、localhost
网络的通信,本质上是两个进程的通信。每台计算机都有很多的进程。如果说IP地址时标识网络中的设备,那么端口号就可以唯一标识设备中的进程。
端口号:用2个字节表示的整数,它的取值范围是0~65535 。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另一个服务或应用占用,会导致应用程序启动失败。
常见端口号:
TCP通信能实现计算机之间的数据交互,通信的两端,要严格区分客户端(Client)和服务端(Server)
在Java中,提供了两个类用于实现TCP通信程序:
java.net.Socket
类表示。创建Socket
对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信java.net.ServerSocket
类表示。创建ServerSocket
对象,相当于开启一个服务,并等待客户端的连接客户端和服务端进行逻辑连接,这个连接中包含一个IO对象,同过IO对象进行通信,因通信不仅仅是字符所以IO对象是字节流对象。
注意:
服务端没有字节流对象,是通过获取客户端的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();
}
}
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();
}
}
注意:
文件上传的原理就是文件的复制。
客户端:
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();
}
}
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);
优化内容:
服务端:
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关闭异常");
}
}
});
}
}
}
模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果
服务器:
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,获取到网页了(一定要注意访问地址正不正确)