一、软件结构
CS结构:全称为Client/Server结构,指的是客户端和数据库服务器结构,常见如QQ、迅雷等。
好处 : 客户端可以完成一些数据的处理, 从而减轻服务器的压力.
缺点 : 需要两套开发人员. 客户端一套人员, 服务端一套人员. 公司的成本很高, 如果程序出现问题, 维护性较差. 并且客户端的软件需要用户自己下载.
BS结构:全称为Browser/Server结构,指的是浏览器和数据库服务器结构,常见如IE、谷歌、火狐等。
好处 : 浏览器是每个操作自带的. 不需要公司单独来开发. 因此, 公司只需要一套开发人员, 节约成本. 维护性较高.
缺点 : 服务器压力较大, 因为所有数据都需要服务端自己解决.
两种架构各有优势,但是无论哪一种,都离不开网络的支持。网络编程就是在一定的协议下,实现两台计算机之间通信的程序。
二、网络通信协议
网络通信协议:通信协议是对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守,最终完成数据交换。
网络的 TCP / IP 结构 :
传输层 :
TCP 协议. Transmission Control Protocol 传输控制协议. (电话机 / 手机)
该协议可以保证数据的安全性和完整性. 通信效率较低.
应用场景 : 上传, 下载, HTTP 协议底层就是 TCP 协议. (HTTP 应用层中浏览器与服务器交互协议)
UDP 用户数据报协议. User Datagram Protocal 用户数据报协议. (对讲机)
该协议无法保证数据的安全性与完整性. 通信效率高.
应用场景 : QQ聊天, 视频会议, 音频会议, 共屏.
TCP 协议的三次握手
: 目的, 确立连接
请求 : 浏览器访问服务器.
响应 : 服务器回应浏览器.
三、网络编程三要素
网络编程三要素分表示:协议、IP地址和端口。
IP 地址 : 指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。类似于每个人的 身份证号码
. 使用IP地址可以寻找到网络上的唯一一台计算机.
Java语言使用 InetAddress 类来表示 IP 对象.
请问 : 如果查看本机的 IP 地址呢 ?
查看 IP 地址, 输入cmd在DOS窗口使用 ipconfig 指令即可 。
IP地址分类
IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。
IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。
有资料显示,全球IPv4地址在2011年2月分配完毕。
为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成 ABCD:EF01:2345:6789:ABCD:EF01:2345:6789 ,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
端口号 : 如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。
端口号用来区分不同的程序.
端口号使用两个字节来表示, 范围 0 ~ 65535.
线程就可以指定一个端口. 自己编写程序可以指定 端口号
.如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。
利用协议+ IP地址+ 端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。
四、TCP 通讯程序
TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。
两端通信时步骤:
1. 服务端程序,需要事先启动,等待客户端的连接。
2. 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
在Java中,提供了两个类用于实现TCP通信程序:
1. 客户端
: java.net.Socket 类表示。创建Socket 对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
2.服务端
: java.net.ServerSocket 类表示。创建ServerSocket 对象,相当于开启一个服务,并等待客户端的连接。
五、简单的TCP通信程序
TCP通信程序的基本步骤
TCP通信分析图解
1.【服务端】启动,创建ServerSocket对象,等待连接。
2. 【客户端】启动,创建Socket对象,请求连接。
3. 【服务端】接收连接,调用accept方法,并返回一个Socket对象。
4. 【客户端】Socket对象,获取OutputStream,向服务端写出数据。
5. 【服务端】Scoket对象,获取InputStream,读取客户端发送的数据。
客户端和服务端之间发送数据案例:
服务端实现 :
// TCP 服务端
public class TCPServerTest1 {
public static void main(String[] args) throws IOException {
// 1. 创建一个服务端套接字
ServerSocket serverSocket = new ServerSocket(8888);
// 2. 等待客户端连接请求 (死等)
Socket socket = serverSocket.accept();
// 3. 接收数据 (输入流读取)
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = -1;
while ((len = in.read(buf)) != -1) {
// 需求 : 将字节数与长度转为字符串线程. (编码)
String str = new String(buf, 0, len);
String ip = socket.getInetAddress().getHostAddress();
System.out.println("客户端发送数据为 : " + ip + " = " + str);
}
//4. 关闭资源
serverSocket.close();
}
}
客户端实现 :
// TCP 客户端
public class TCPClientTest1 {
public static void main(String[] args) throws IOException {
// 1. 创建一个客户端套接字
// 给自己发送数据的写法 : localhost 本地主机 127.0.0.1 本机回环地址 192.168.29.67 IP地址
Socket socket = new Socket("192.168.29.67", 8888);
// 2. 向服务端发送数据 (输出流写出)
OutputStream out = socket.getOutputStream();
out.write("小姐姐, 约吗 ?".getBytes());
out.flush();
// 向通道中写出结束符
socket.shutdownOutput();
// 3. 接收服务端的回送数据 (输入流)
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = -1;
while ((len = in.read(buf)) != -1) {
String str = new String(buf, 0, len);
System.out.println("服务端回送数据为 : " + str);
}
// 4. 关闭资源
socket.close();
}
}
文件上传案例:
文件上传分析图解
1. 【客户端】输入流,从硬盘读取文件数据到程序中。
2. 【客户端】输出流,写出文件数据到服务端。
3. 【服务端】输入流,读取文件数据到服务端程序。
4. 【服务端】输出流,写出文件数据到服务器硬盘中。
服务端实现
public class TCPUploadServer {
public static void main(String[] args) throws IOException {
// 1. 创建一个服务端的套接字
ServerSocket serverSocket = new ServerSocket(8888);
// 2. 等待客户端的连接请求 (等待)
Socket socket = serverSocket.accept();
// 需求 : 复制图片文件
// 3.1 创建一个高效的缓冲字节输入流
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
// 3.2 创建一个高效的缓冲字节输出流
// 3.2.1 先创建一个父目录文件对象
File parentFile = new File("C:\\Users\\Desktop\\upload");
// 3.2.2 判断父目录是否存在
if (parentFile.exists() == false) {
// 3.2.3 父目录不存在, 需要被创建
parentFile.mkdirs();
}
// 3.2.4 创建子文件对象
// 图片名称 : ip地址
String ip = socket.getInetAddress().getHostAddress();
File file = new File(parentFile, ip + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
// 3.3 读写操作
byte[] buf = new byte[1024];
int len = -1;
while ((len = bis.read(buf)) != -1) {
bos.write(buf, 0, len);
bos.flush();
}
// 4. 回送数据给客户端 (输出流)
OutputStream out = socket.getOutputStream();
out.write("上传成功!".getBytes());
out.flush();
// 5. 向通道中写出一个结束符
socket.shutdownOutput();
// 6. 关闭资源
bos.close();
serverSocket.close();
}
}
客户端实现
public class TCPUploadClient {
public static void main(String[] args) throws IOException {
// 1. 创建一个客户端套接字
Socket socket = new Socket("192.168.29.67", 8888);
// 需求 : 图片文件复制
// 2.1 创建一个高效的缓冲字节输入流
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("C:\\Users\\Pictures\\7.jpg"));
// 2.2 创建一个高效的缓冲字节输出流
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
// 2.3 读写操作
byte[] buf = new byte[1024];
int len = -1;
while ((len = bis.read(buf)) != -1) {
bos.write(buf, 0, len);
bos.flush();
}
// 2.4 向通道中写出一个结束符
socket.shutdownOutput();
// 3. 客户端接收服务端的回送信息
InputStream in = socket.getInputStream();
byte[] readBuf = new byte[1024];
int readLen = -1;
while ((readLen = in.read(readBuf)) != -1) {
String str = new String(readBuf, 0, readLen);
System.out.println(str);
}
// 4. 关闭资源
bis.close();
socket.close();
}
}
文件上传优化
1.进行多次上传,我们发现,每次上传都只能上传一个文件,而且一旦上传结束,服务器就关闭了,如果想同时上传多个数据,应该怎么办?
这里问题有两点:
1.1、循环接收的问题
服务器不应该使用一次就关闭,应该在任何时候都能接收服务器的请求。如何保证服务器常开状态?
// 每次接收新的连接,创建一个Socket
while(true){
Socket accept = serverSocket.accept();
}
1.2、效率问题
文件上传是一个耗时操作,每次只能上传一个文件会导致其他文件必须等待前面的文件上传结束后才能上传,效率太低,如何解决?
利用多线程技术!
将每一次上传都看作一个子线程,这样就不用等待文件上传结束之后再进行下一个文件上传。
while(true){
Socket accept = serverSocket.accept();
// accept 交给子线程处理.
new Thread(() ‐> {
......
InputStream bis = accept.getInputStream();
......
}).start();
}
至此,我们已经实现了多文件同时上传。
然而,我们进行多次上传发现,每次客户端上传都成功了,但是在服务端却只存在一个文件,这是为什么呢?
String ip = socket.getInetAddress().getHostAddress();
File file = new File(parentFile, ip + ".jpg");
原因就在这里:每次上传的文件在保存到服务端的时候,文件名都是一样的,这样就会发生后上传的文件覆盖前面上传的文件的现象。这要如何解决呢?
解决方法1:
使用系统时间优化,保证名称唯一
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis()+".jpg") // 文件名称
解决方法2:
Java提供了一个UUID类来解决文件命名问题。可以给每一个文件设置通用唯一标识符。避免文件名称重复现象
代码实现
String name = UUID.randomUUID().toString().replaceAll("-", "");
File file = new File(parentFile, name + ".jpg");
文件上传案例优化后的代码实现:
服务端:
public class TCPUploadServer3 {
public static void main(String[] args) throws IOException {
// 1. 创建一个服务端的套接字
ServerSocket serverSocket = new ServerSocket(8888);
// 循环接收
while (true) {
// 2. 等待客户端的连接请求 (等待)
Socket socket = serverSocket.accept();
// 3. 子线程执行图片复制耗时操作
new Thread(new UploadPictureTask(socket), "图片上传线程").start();
}
// 服务端一直等待接收
// serverSocket.close();
}
}
实现类 UploadPictureTask:
public class UploadPictureTask implements Runnable {
// 属性
private Socket socket;
// 构造方法
public UploadPictureTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 需求 : 复制图片文件 (耗时操作) 子线程单独执行
// 3.1 创建一个高效的缓冲字节输入流
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
// 3.2 创建一个高效的缓冲字节输出流
// 3.2.1 先创建一个父目录文件对象
File parentFile = new File("C:\\Users\\Desktop\\upload");
// 3.2.2 判断父目录是否存在
if (parentFile.exists() == false) {
// 3.2.3 父目录不存在, 需要被创建
parentFile.mkdirs();
}
// 3.2.4 创建子文件对象
// 图片名称 : UUID Java语言提供的唯一标识符类
String name = UUID.randomUUID().toString().replaceAll("-", "");
File file = new File(parentFile, name + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
// 3.3 读写操作
byte[] buf = new byte[1024];
int len = -1;
while ((len = bis.read(buf)) != -1) {
bos.write(buf, 0, len);
bos.flush();
}
// 4. 回送数据给客户端 (输出流)
OutputStream out = socket.getOutputStream();
out.write("上传成功!".getBytes());
out.flush();
// 5. 向通道中写出一个结束符
socket.shutdownOutput();
// 6. 关闭资源
bos.close();
} catch (IOException e) {
}
}
}
客户端:
public class TCPUploadClient {
public static void main(String[] args) throws IOException {
// 1. 创建一个客户端套接字
Socket socket = new Socket("192.168.29.67", 8888);
// 需求 : 图片文件复制
// 2.1 创建一个高效的缓冲字节输入流
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("C:\\Users\\Pictures\\7.jpg"));
// 2.2 创建一个高效的缓冲字节输出流
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
// 2.3 读写操作
byte[] buf = new byte[1024];
int len = -1;
while ((len = bis.read(buf)) != -1) {
bos.write(buf, 0, len);
bos.flush();
}
// 2.4 向通道中写出一个结束符
socket.shutdownOutput();
// 3. 客户端接收服务端的回送信息
InputStream in = socket.getInputStream();
byte[] readBuf = new byte[1024];
int readLen = -1;
while ((readLen = in.read(readBuf)) != -1) {
String str = new String(readBuf, 0, readLen);
System.out.println(str);
}
// 4. 关闭资源
bis.close();
socket.close();
}
}
一个简单的文件上传案例就完成了。