1. 网络编程
网络编程可以直接或间接地通过网络协议与其他计算机进行通信。在Java中包含网络编程地各种类,通过创建这些类的对象,调用相应的方法,就可以进行网络应用程序的编写。
软件结构
- C/S结构:全称为Client/Server结构,是指客户端和服务器结构。
- B/S结构:全称为Browser/Server结构,是指浏览器和服务器结构。
网络通信协议: 通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则。
TCP/IP协议: 传输控制协议/因特网互联协议,是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在他们之间传递的标准。它的内部包换一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。
- 应用层:向用户提供一组常用的应用程序。
- 传输层:提供应用程序间的通信。
- 网络层:负责相邻计算机之间的通信。
- 网络接口层:这是TCP/IP软件的最低层,负责接收IP数据报并通过网络发送之,或者从网络上接收物理帧,抽出IP数据报,交给IP层。
网络通信协议分类
java.net包中提供了两种常见的网络协议的支持
-
传输控制协议(TCP): TCP是面向连接的通信协议,即传输数据前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机间可靠无差错的数据传输,每次连接都需要经过“三次握手:。
特点:提供的是面向连接、可靠的字节流服务。 -
用户数据报协议(UDP): UDP是一个简单的面向数据报的传输层协议。提供的是非面向连接的、不可靠的数据流传输。UDP不提供可靠性,也不提供报文到达确认、排序以及流量控制等功能。它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。因此报文可能会丢失、重复以及乱序等。但由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。
特点:数据被限制在64kb以内,超过就不能发送了
IP地址
对网络编程来说,主要是计算机和计算机之间的通信,首要的问题就是如何找到网络上数以亿计的计算机。为了解决这个问题,网络中的每个设备都会有唯一的数字标识,也就是 IP 地址。
分类: IPv4、IPv6
端口号
使用 IP 地址只能定位到某一台计算机,却不能准确地连接到想要连接的服务器。通常使用一个 0~65535 的整数来标识该机器上的某个服务,这个整数就是端口号(Port)。端口号并不是指计算机上实际存在的物理位置,而是一种软件上的抽象。
主要分为:
- 由 Internet 名字和号码指派公司 ICANN 分配绐一些常用的应用层程序固定使用的熟知端口,其值是 0~1023。例如 HTTP 服务一般使用 80 端口,FTP 服务使用 21 端口。
- 一般端口用来随时分配绐请求通信的客户进程。
1.1 TCP通信
概述
TCP通信可以实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端和服务端。
在Java中,提供了两个类用于实现TCP通信程序
- 客户端:java.net.Socket类表示。创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信;
- 服务端:java.net.ServerSocket类表示。创建ServerSocket对象,相当于开启一个服务,并等待客户端的连接。
服务器端必须明确两件事情: - 多个客户端同时和服务器进行交互,服务器必须明确和哪个客户端进行的交互。在服务器端中有一个方法,叫accept客户端获取到请求的客户端对象。
- 多个客户端同时和服务器进行交互,就需要多个IO流对象。服务器是没有IO流的,服务器可以获取到请求的客户端对象Socket,使用每个客户端中提供的IO流和客户端进行交互(服务器使用客户端的字节输入流读取客户端发送的数据,使用客户端的字节输出流给客户端回写数据)。
1.2 客户端代码实现
TCP通信的客户端需要实现:向服务器发送连接请求、给服务器发送数据、读取服务器回写的数据。
表示客户端的类:
java.net.Socket,此类实现客户端套接字。套接字是两台机器间通信的端点,是包含了IP地址和端口号的网络单位。
构造方法:
Socket(String host, int port),创建一个套接字并将其连接到指定主机上的指定端口号。
参数:
- String host:服务器主机的名称/服务器的IP地址。
- int port:服务器的端口号。
成员方法:
- OutputStream getOutputStream():返回此套接字的输出流;
- InputStream getInputStream():返回此套接字的输入流;
- void close():关闭此套接字。
public class TCPClient {
public static void main(String[] args) throws IOException {
//1. 创建一个客户端对象Socket,构造方法绑定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8888);
//2. 使用getOutputStream()获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
//3. 使用网络字节输出流OutputStream对象中的方法write(),给服务器发送数据
os.write("你好服务器".getBytes());
//4. 使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//5. 使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes, 0, len));
//6. 释放资源
socket.close();
}
}
注意:
- 客户端和服务器端进行交互,必须使用Socket中提供的网络流,不能使用自己创建的流对象;
- 当我们创建客户端Socket的时候,就回去请求服务器和服务器进行3次握手建立连接通路,这时如果服务器没有启动,那么就会抛出异常;如果服务器已经启动,那么就可以进行交互了。
1.3 服务器端的代码实现
TCP通信的服务器端需要实现:接收客户端的需求、读取客户端发送的数据、给客户端回写数据。
表示服务器的类:
java.net.ServerSocket:此类实现服务器套接字。
构造方法:
ServerSocket(int port):创建绑定到特定端口的服务器套接字。
成员方法:
Socket accept():侦听并接受到此套接字的连接。
public class TCPServer {
public static void main(String[] args) throws IOException {
//1. 创建服务器端对象ServerSocket和系统要指定的端口号
ServerSocket server = new ServerSocket(8888);
//2. 使用accept获取到请求的客户端对象
Socket socket = server.accept();
//3. 使用getInputStream获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//4. 使用网络字节输入流InputStream中的方法read,读取客户端发送的数据
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes, 0, len));
//5. 使用Socket中的方法getOutputStream()获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
//6. 使用write()给客户端回写数据
os.write("收到".getBytes());
//7. 释放资源
socket.close();
server.close();
}
}
1.4 文件上传案例
要求:
客户端上传C:\1.jpg文件到服务器,服务器读取文件并保存在D:\upload并给客户端回写“上传成功”
客户端:
public class TCPClient {
public static void main(String[] args) throws IOException {
//1. 创建一个客户端对象Socket,构造方法绑定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8888);
//2. 创建一个本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("C:\\1.jpg");
//3. 使用getOutputStream()获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
//4. 使用本地字节输入流FileInputStream中的方法read,读取本地文件
byte[] bytes = new byte[1024];
int len;
while((len = fis.read(bytes)) != -1){
//5. 使用网络字节输出流OutputStream对象中的方法write(),把读取到的文件发送给服务器
os.write(bytes, 0, len);
//最后不会上传结束语句,所以要再写一个结束标记
}
socket.shutdownOutput();
//6. 使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//7. 使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
while((len = is.read(bytes)) != -1){
System.out.println(new String(bytes, 0, len));
}
//8. 释放资源
fis.close();
socket.close();
}
}
服务器端:
public class TCPServer {
public static void main(String[] args) throws IOException {
//1. 创建服务器端对象ServerSocket和系统要指定的端口号
ServerSocket server = new ServerSocket(8888);
//2. 使用accept获取到请求的客户端对象
Socket socket = server.accept();
//3. 使用getInputStream获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//4. 判断D:\\upload文件夹是否存在,不在则建立一个
File file = new File("D:\\upload");
if(!file.exists()){
file.mkdirs();
}
//5. 创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
FileOutputStream fos = new FileOutputStream(file + "\\1.jpg");
//6. 使用网络字节输入流InputStream中的方法read,读取客户端发送的数据
byte[] bytes = new byte[1024];
int len = 0;
while((len = is.read(bytes)) != -1){
// 7.把读取的文件保存到服务器的硬盘上
fos.write(bytes, 0, len);
}
//8. 使用Socket中的方法getOutputStream()获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
//9. 使用write()给客户端回写数据
os.write("上传成功".getBytes());
//10. 释放资源
fos.close();
socket.close();
server.close();
}
}
2. 函数式接口
函数式接口: 有且仅有一个抽象方法的接口。函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lanbda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
格式:
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
//其他非抽象方法内容
}
@FunctionalInterface注解:可以检测接口是否是一个函数式接口。
函数式接口一般可以作为方法的参数和返回值类型
@FunctionalInterface
public interface MyFunctionalInterface {
//定义一个抽象方法
public abstract void method();
}
public class MyFunctionalInterfaceImpl implements MyFunctionalInterface {
@Override
public void method() {
}
}
public class Demo {
public static void show(MyFunctionalInterface myInter){
myInter.method();
}
public static void main(String[] args) {
//调用show方法,方法的参数是一个接口,所以可以传递接口的实现类对象
show(new MyFunctionalInterfaceImpl());
//调用show方法,方法的参数是一个接口,所以可以传递接口的匿名内部类
show(new MyFunctionalInterface() {
@Override
public void method() {
System.out.println("使用匿名内部类重写接口中的抽象方法");
}
});
//调用show方法,方法的参数是一个函数式接口,所以可以用Lambda表达式
show(() -> System.out.println("使用匿名内部类重写接口中的抽象方法"));
}
}
2.1 Lambda的延迟执行
某些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这种好可以作为解决方案,提升性能。
案例:
//如果level不为1,那么字符串就白拼接了,存在浪费
public class DemoLogger {
public static void showLog(int level, String message){
if(level == 1){
System.out.println(message);
}
}
public static void main(String[] args) {
String mes1 = "Hello";
String mes2 = "World";
String mes3 = "!";
showLog(2, mes1+mes2+mes3);
}
}
优化案例:
@FunctionalInterface
public interface MessageBuilder {
public abstract String Builder();
}
/*
Lambda优点:延迟加载
使用前提:必须存在函数式接口
*/
public class DemoLogLambda {
public static void showLog(int level, MessageBuilder mb){
if(level == 1){
System.out.println(mb.Builder());
}
}
public static void main(String[] args) {
String mes1 = "Hello";
String mes2 = "World";
String mes3 = "!";
showLog(1, ()->{
return mes1+mes2+mes3;
});
/*
使用Lambda表达式作为参数传递,仅仅是把参数传递到showLog方法中,
只有满足level == 1,才会调用接口MessageBuilder中的方法Builder进行字符串拼接,
所以不会造成性能的浪费
*/
}
}
2.2 函数式接口作为方法的参数和返回值
例如java.lang.Runnable接口就是一个函数式接口,假设有一个startThread方法使用该接口作为参数,那么就可以使用Lambda传参。
public class Demo01Runnable {
public static void startThread(Runnable run) {
new Thread(run).start();
}
public static void main(String[] args) {
startThread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程启动了");
}
});
startThread(() -> System.out.println(Thread.currentThread().getName() + "线程启动了"));
}
}
如果一个方法的返回值类型是一个函数式参数,那么就可以直接返回一个Lambda表达式。当需要通过一个方法来获取java.util.Comparator接口类型的对象作为排序器时,就可以调用该方法获取。
public class Demo01Comparator {
public static Comparator<String> getComparator() {
/*
return new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
//按字符串长度排序
return o1.length() - o2.length();
}
};
*/
return (o1, o2) -> o2.length() - o1.length();
}
public static void main(String[] args) {
String[] arr = {"a", "ab", "abc"};
System.out.println(Arrays.toString(arr));
Arrays.sort(arr, getComparator());
System.out.println(Arrays.toString(arr));
}
}
2.3 常用的函数式接口
1. Supplier接口
java.util.function.Supplier< T>接口仅包含一个无参方法:
T get():用来获取一个泛型参数指定类型的对象数据
Supplier< T>接口被称为生产型接口,这是因为被指定接口的泛型是什么类型,那么接口的get方法就会生产什么类型的数据。
public class DemoSupplier {
public static String getString(Supplier<String> s){
return s.get();
}
public static void main(String[] args) {
String s = getString(() -> {
return "字符串";
});
System.out.println(s);
}
}
练习:使用Supplier接口作为方法参数类型,通过Lambda表达式求出int类型数组中的最大值
public class MaxValue {
public static int getMax(Supplier<Integer> s){
return s.get();
}
public static void main(String[] args) {
int[] arr = {5, 17, 22, 3, 4};
int m = getMax(() -> {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if(max < arr[i])
max = arr[i];
}
return max;
});
System.out.println(m);
}
}
2. Consumer接口
java.util.function.Consumer< T>接口正好与Supplier接口相反,他不是产生一个数据,而是消耗一个数据,其数据类型由泛型决定。
void accept(T t):消费一个指定泛型的数据。
Consumer接口是一个消费性接口,泛型执行什么类型,就可以使用accept方法消费什么类型的数据,具体怎么使用,需要自定义。
public class DemoConsumer {
public static void method(String name, Consumer<String> c){
c.accept(name);
}
public static void main(String[] args) {
method("猪猪侠", (String name) -> {
System.out.println(name);
});
}
}
默认方法:andThen
作用:需要两个Consumer接口,可以把两个Consumer接口组合到一起,再对数据进行消费。
public class DemoAndThen {
public static void method(String name, Consumer<String> c1, Consumer<String> c2){
/*
c1.accept(name);
c2.accept(name);
*/
//使用andThen方法,把两个Consumer连接到一起,再消费
c1.andThen(c2).accept(name);
}
public static void main(String[] args) {
method("Hello",
(c1) -> {
//消费方式:转换为大写输出
System.out.println(c1.toUpperCase());
},
(c2) -> {
//消费方式:转换为小写输出
System.out.println(c2.toLowerCase());
});
}
}
3. Predicate接口
java.util.function.Predicate< T>接口
作用: 对某种数据类型的数据进行判断,返回一个boolean值
抽象方法:
boolean test(T t):对指定数据进行判断,符合条件,则返回true
public class DemoPredicate {
public static boolean checkString(String s, Predicate<String> p) {
return p.test(s);
}
public static void main(String[] args) {
String s = "abcde";
boolean b = checkString(s, (String str)->{
return str.length() == 5;
});
System.out.println(b);
}
}
默认方法:
Predicate接口有一个方法and,表示并且关系,可以用于连接两个判断条件(等价于&&)。
public class DemoPredicate {
public static boolean checkString(String s, Predicate<String> p1, Predicate<String> p2) {
return p1.and(p2).test(s); //相当于p1.test(s) && p2.test(s)
}
public static void main(String[] args) {
String s = "abcde";
boolean b = checkString(s,
(String str1)->{
//判断长度是否等于5
return str1.length() == 5;
}, (String str2) -> {
//判断是否含有a
return str2.contains("a");
}) ;
System.out.println(b);
}
}
方法or,表示或者关系,可以用于连接两个判断条件(等价于||)。
方法negate,取反,等价于!
4. Function接口
java.util.function.Function<T, R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
R apply(T t):根据类型T的参数获取类型R的结果。
public class DemoFunction {
public static void change(String s, Function<String, Integer> f){
Integer in = f.apply(s);
System.out.println(in);
}
public static void main(String[] args) {
String s = "1234";
change(s, (String str) -> Integer.parseInt(str));
}
}
默认方法andThen:用来进行组合操作。
public class DemoFunction {
//把String类型的“1234”转化为Integer类型,再将转换后的结果+10,把增加后的Integer类型再转换为String类型,再加a
public static void change(String s, Function<String, Integer> f1, Function<Integer, String> f2){
String ss = f1.andThen(f2).apply(s);
System.out.println(ss);
}
public static void main(String[] args) {
String s = "1234";
change(s,
(String str) -> {
return Integer.parseInt(str) + 10;
},
(Integer i) -> {
return i + "a";
});
}
}