TCP/IP协议泛指互联网协议,其中最重要的两个协议是TCP协议和IP协议。只有使用TCP/IP协议的计算机才能够联入互联网。
IP地址:一个IP地址用于唯一标识一个网络接口(Network Interface)。一台联入互联网的计算机肯定有一个IP地址,但也可能有多个IP地址。(多块网卡,虚拟机等)。
本机地址总是127.0.0.1,还有一个IP地址(和网卡关联),例如101.202.99.12,用来接入网络。
IP地址 & 子网掩码——>网络号(是否在同一个子网中)
域名:域名解析服务器DNS负责把域名翻译成对应的IP,客户端再根据IP地址访问服务器。
本机域名localhost,对应的IP地址总是本机地址127.0.0.1
网络模型:五层模型,复习计算机网络去
常用协议:
- IP协议:一种分组交换传输协议;不保证可靠传输;负责发数据包,不保证顺序性和正确性
- TCP协议:一种面向连接,可靠传输的协议;建立在IP协议之上,负责控制数据包传输
- UDP协议:一种无连接,不可靠传输的协议。
TCP编程
Socket:{IP地址:端口号} ——两个进程间通信:需要IP地址,也需要端口号
Socket连接成功地在服务器端和客户端之间建立后:
对服务器端来说,主动监听指定端口,它的Socket是指定的IP地址和指定的端口号;
对客户端来说,主动连接服务端的IP地址和指定端口,它的Socket是它所在计算机的IP地址和一个由操作系统分配的随机端口号。
服务器端
Java标准库提供了ServerSocket来实现对指定IP和指定端口的监听。典型服务端代码如下:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(6666); // 监听指定端口6666
System.out.println("server is running...");
//使用无限循环来处理客户端的连接
for (;;) {
//每当有新客户端连接,就返回一个Socket实例,只要用循环不断调用accept()就可以获取新连接
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
Thread t = new Handler(sock);
//start()会自动调用run()方法
t.start();
}
}
}
class Handler extends Thread {
Socket sock;
public Handler(Socket sock) {
this.sock = sock;
}
@Override
public void run() {
try (InputStream input = this.sock.getInputStream()) {
try (OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
}
} catch (Exception e) {
try {
this.sock.close();
} catch (IOException ioe) {
}
System.out.println("client disconnected.");
}
}
//handle就很简单,就是把所有输入流写出到输出流
private void handle(InputStream input, OutputStream output) throws IOException {
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
writer.write("hello\n");
writer.flush();
for (;;) {
String s = reader.readLine();
if (s.equals("bye")) {
writer.write("bye\n");
writer.flush();
break;
}
writer.write("ok: " + s + "\n");
writer.flush();
}
}
}
客户端
public class Client {
public static void main(String[] args) throws IOException {
Socket sock = new Socket("localhost", 6666); // 连接指定服务器和端口,连接成功返回Socket实例,用于后续通信
try (InputStream input = sock.getInputStream()) {
try (OutputStream output = sock.getOutputStream()) {
handle(input, output);
}
}
sock.close();
System.out.println("disconnected.");
}
//handle()方法逐行打印输入流
private static void handle(InputStream input, OutputStream output) throws IOException {
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
Scanner scanner = new Scanner(System.in);
System.out.println("[server] " + reader.readLine());
for (;;) {
System.out.print(">>> "); // 打印提示
String s = scanner.nextLine(); // 读取一行输入
writer.write(s);
writer.newLine();
writer.flush();
String resp = reader.readLine();
System.out.println("<<< " + resp);
if (resp.equals("bye")) {
break;
}
}
}
}
Socket流
Java标准库使用InputStream和OutputStream来封装Socket的数据流,使用Socket的流,和普通IO流类似:
// 用于读取网络数据:
InputStream in = sock.getInputStream();
// 用于写入网络数据:
OutputStream out = sock.getOutputStream();
写入网络数据流要用flush()方法,强制把缓冲区的数据发送到网络上(即时通信)
UDP编程
UDP没有创建连接,Java使用UDP编程仍然需要使用Socket,因为应用程序在使用UDP时必须指定网络接口(IP)和端口号。UDP端口和TCP端口都使用0~65535,但他们是两套独立的端口,使用不会互斥。
服务器端
Java提供了DatagramSocket来监听指定的端口:
DatagramSocket ds = new DatagramSocket(6666); // 监听指定端口
for (;;) { // 无限循环处理UDP数据包
// 数据缓冲区:buffer
byte[] buffer = new byte[1024];
//通过DatagramPacket实现接收UDP数据包
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet); // 收取一个UDP数据包
// 收取到的数据存储在buffer中,由packet.getOffset(), packet.getLength()确定数据包在buffer缓冲区的起始位置和长度
// 假设收取的是一个String,将其按UTF-8编码转换为String:
String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
// 要回复“消息”,发送数据:
byte[] data = "ACK".getBytes(StandardCharsets.UTF_8);
//通过DatagramPacket发送回应的UDP包
packet.setData(data);
ds.send(packet);
}
客户端
客户端使用UDP时,只需要直接向服务器端发送UDP包,然后接收返回的UDP包(不需要监听端口):
//客户端创建DatagramSocket实例时并不需要指定端口,而是由操作系统自动指定一个当前未使用的端口
DatagramSocket ds = new DatagramSocket();
//设定超时1秒,接受UDP包等待时间最多不会超过1秒,客户端不应该无限等待,而服务端就是设计为长时间运行的
ds.setSoTimeout(1000);
//connect()方法不是真连接,保存服务端的IP和端口,只往指定地址和端口发送UDP包
//客户端希望向两个不同的服务器发送UDP包,那么它必须创建两个DatagramSocket实例。
ds.connect(InetAddress.getByName("localhost"), 6666); // 连接指定服务器和端口
// 发送:
byte[] data = "Hello".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length);
ds.send(packet);
// 接收:
byte[] buffer = new byte[1024];
packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);//利用DatagramSocket 接收packet
String resp = new String(packet.getData(), packet.getOffset(), packet.getLength());
//disconnect()也不是真正地断开连接,它只是清除了客户端DatagramSocket实例记录的远程服务器地址和端口号
ds.disconnect();
小结
使用UDP协议通信时,服务器和客户端双方无需建立连接:
服务器端用DatagramSocket(port)监听端口;
客户端使用DatagramSocket.connect()指定远程地址和端口;
双方通过receive()和send()读写数据;
DatagramSocket没有IO流接口,数据被直接写入byte[]缓冲区。
发送Email
类似Outlook这样的邮件软件称为MUA:Mail User Agent,意思是给用户服务的邮件代理;
邮件服务器则称为MTA:Mail Transfer Agent,意思是邮件中转的代理;
最终到达的邮件服务器称为MDA:Mail Delivery Agent,意思是邮件到达的代理。电子邮件一旦到达MDA,就不再动了。实际上,电子邮件通常就存储在MDA服务器的硬盘上,然后等收件人通过软件或者登陆浏览器查看邮件。
MTA和MDA这样的服务器软件通常是现成的。关心如何编写一个MUA的软件,把邮件发送到MTA上。
MUA到MTA发送邮件的协议就是SMTP协议。
准备SMTP登录信息
邮件服务器地址通常是smtp.example.com,端口号由邮件服务商确定使用25、465还是587。以下是一些常用邮件服务商的SMTP信息:
QQ邮箱:SMTP服务器是smtp.qq.com,端口是465/587;
163邮箱:SMTP服务器是smtp.163.com,端口是465;
Gmail邮箱:SMTP服务器是smtp.gmail.com,端口是465/587。
有了SMTP服务器的域名和端口号,我们还需要SMTP服务器的登录信息,通常是使用自己的邮件地址作为用户名,登录口令是用户口令或者一个独立设置的SMTP口令。
使用JavaMail发送邮件:待填坑
- 发送邮件
- 发送HTML邮件
- 发送附件
- 发送内嵌图片的HTML邮件
小结
使用JavaMail API发送邮件本质上是一个MUA软件通过SMTP协议发送邮件至MTA服务器;
打开调试模式可以看到详细的SMTP交互信息;
某些邮件服务商需要开启SMTP,并需要独立的SMTP登录密码。
接收Email
客户端总是通过SMTP协议把邮件发送给MTA
接收邮件是自己的客户端从MDA服务器上抓取到本地的过程,接受邮件使用最广泛的协议是POP3——建立在TCP连接上的协议。
另一种接收邮件的协议是IMAP:Internet Mail Access Protocol
IMAP和POP3的主要区别是,IMAP协议在本地的所有操作都会自动同步到服务器上,并且,IMAP可以允许用户在邮件服务器的收件箱中创建文件夹。
小结
使用Java接收Email时,可以用POP3协议或IMAP协议。
使用POP3协议时,需要用Maven引入JavaMail依赖,并确定POP3服务器的域名/端口/是否使用SSL等,然后,调用相关API接收Email。
设置debug模式可以查看通信详细内容,便于排查错误。
HTTP编程
HTTP——超文本传输协议,基于TCP协议之上的一种请求-响应协议。
浏览器请求访问某个网站时发送的HTTP请求-响应。当浏览器希望访问某个网站时,浏览器和网站服务器之间首先建立TCP连接,且服务器总是使用80端口和加密端口443,然后,浏览器向服务器发送一个HTTP请求,服务器收到后,返回一个HTTP响应,并且在响应中包含了HTML的网页内容,这样,浏览器解析HTML后就可以给用户显示网页了。一个完整的HTTP请求-响应如下:
HTTP请求的格式是固定的,它由HTTP Header和HTTP Body(请求头+请求体)两部分构成。第一行总是请求方法 路径 HTTP版本,例如,GET / HTTP/1.1表示使用GET请求,路径是/,版本是HTTP/1.1。
后续的每一行都是固定的Header: Value格式,我们称为HTTP Header,服务器依靠某些特定的Header来识别客户端请求,例如:
Host:表示请求的域名,因为一台服务器上可能有多个网站,因此有必要依靠Host来识别用于请求;
User-Agent:表示客户端自身标识信息,不同的浏览器有不同的标识,服务器依靠User-Agent判断客户端类型;
Accept:表示客户端能处理的HTTP响应格式,*/*表示任意格式,text/*表示任意文本,image/png表示PNG格式的图片;
Accept-Language:表示客户端接收的语言,多种语言按优先级排序,服务器依靠该字段给用户返回特定语言的网页版本。
如果是GET请求,那么该HTTP请求只有HTTP Header,没有HTTP Body。如果是POST请求,那么该HTTP请求带有Body,以一个空行分隔。一个典型的带Body的HTTP请求如下:
GET请求的参数必须附加在URL上,并以URLEncode方式编码,例如:http://www.example.com/?a=1&b=K%26R,参数分别是a=1和b=K&R。因为URL的长度限制,GET请求的参数不能太多,而POST请求的参数就没有长度限制,因为POST请求的参数必须放到Body中。并且,POST请求的参数不一定是URL编码,可以按任意格式编码,只需要在Content-Type中正确设置即可。常见的发送JSON的POST请求如下:
HTTP响应也是由Header和Body(响应头+响应体)两部分组成,一个典型的HTTP响应如下:
响应的第一行总是**HTTP版本 响应代码 响应说明,**例如,HTTP/1.1 200 OK表示版本是HTTP/1.1,响应代码是200,响应说明是OK。客户端只依赖响应代码判断HTTP响应是否成功。HTTP有固定的响应代码:
- 1xx:表示一个提示性响应,例如101表示将切换协议,常见于WebSocket连接;
- 2xx:表示一个成功的响应,例如200表示成功,206表示只发送了部分内容;
- 3xx:表示一个重定向的响应,例如301表示永久重定向,303表示客户端应该按指定路径重新发送请求;
- 4xx:表示一个因为客户端问题导致的错误响应,例如400表示因为Content-Type等各种原因导致的无效请求,404表示指定的路径不存在;
- 5xx:表示一个因为服务器问题导致的错误响应,例如500表示服务器内部故障,503表示服务器暂时无法响应。
当浏览器收到第一个HTTP响应后,它解析HTML后,又会发送一系列HTTP请求,例如,GET /logo.jpg HTTP/1.1请求一个图片,服务器响应图片请求后,会直接把二进制内容的图片发送给浏览器。服务器总是被动地接收客户端的一个HTTP请求,然后响应它。客户端则根据需要发送若干个HTTP请求。
HTTP编程
针对客户端编程(获得响应内容)和针对服务端编程(编写web服务器)。
早期的JDK版本是通过HttpURLConnection访问HTTP,典型代码如下:
URL url = new URL("http://www.example.com/path/to/target?a=1&b=2");
//新建连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//请求方式GET
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);//请求超时5秒
//设置HTTP头
conn.setRequestProperty("Accept","*/*");//客户端能处理任意格式的http响应
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MSIE 11; Windows NT 5.1)");//客户端自身标识信息
//连接并发送HTTP请求:
conn.connect();
//判断HTTP响应是否200:200表示成功
if(conn.getResponseCode() != 200) {
throw new RuntimeException("bad response");
}
//获取所有响应Header:
Map<String, List<String>> map = conn.getHeaderFields();
for(String key:map.ketSet()) {//访问map的key集合set
System.out.println(key + ":" + map.get(key));
}
//获取响应内容
InputStream input = conn.getInputStream();
...
旧版本JDK代码编写繁琐,还需要手动处理InputStream。
Java 11 引入了新的HttpClient,使用链式调用的API,简化HTTP的处理。
使用HttpClient:首先需要创建一个全局HttpClient实例,因为HttpClient内部使用线程池优化多个HTTP连接,可以复用。
static HttpClient httpClient = HttpClient.newBuilder().build();
使用GET请求获取文本内容代码如下:
import java.net.URI;
import java.net.http.*;
import java.net.http.HttpClient.Version;
import java.time.Duration;
import java.util.*;
public class Main {
// 全局HttpClient:
static HttpClient httpClient = HttpClient.newBuilder().build();
public static void main(String[] args) throws Exception {
String url = "https://www.sina.com.cn/";
HttpRequest request = HttpRequest.newBuilder(new URI(url))
// 设置Header:
.header("User-Agent", "Java HttpClient").header("Accept", "*/*")
// 设置超时:
.timeout(Duration.ofSeconds(5))
// 设置版本:
.version(Version.HTTP_2).build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());//发送请求,接收响应
// HTTP允许重复的Header,因此一个Header可对应多个Value:
Map<String, List<String>> headers = response.headers().map();
for (String header : headers.keySet()) {//遍历响应Header
System.out.println(header + ": " + headers.get(header).get(0));
}
//输出响应body
System.out.println(response.body().substring(0, 1024) + "...");//输出响应体的前1024个字符
}
}
获取图片这样的二进制内容:把HttpResponse.BodyHander.ofString()换成HttpResponse.BodyHanders.ofByteArray(),就可以获得一个HttpResponse<byte[]>对象。如果响应的内容很大,不希望一次性全部加载到内存,可以使用HttpResponse.BodyHandlers.ofInputStream()获取一个InputStream流。
要使用POST请求(请求参数放在body里,get请求放在header),我们要准备好发送的Body数据并正确设置Content-Type:
String url = "http://www.example.com/login";
String body = "username=bob&password=123456";
HttpRequest request = HttpRequest.newBuilder(new URI(url))
// 设置Header:
.header("Accept", "*/*")
.header("Content-Type", "application/x-www-form-urlencoded")
// 设置超时:
.timeout(Duration.ofSeconds(5))
// 设置版本:
.version(Version.HTTP_2)
// 使用POST并设置Body:
.POST(BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
String s = response.body();
RMI远程调用
Java的RMI远程调用是指,一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法。RMI是Remote Method Invocation的缩写。提供服务的一方我们称之为服务器,而实现远程调用的一方我们称之为客户端。要实现RMI,服务器和客户端必须共享同一个接口。Java的RMI规定此接口必须派生自java.rmi.Remote,并在每个方法声明抛出RemoteException。
小结
Java提供了RMI实现远程方法调用:
RMI通过自动生成stub和skeleton实现网络调用,客户端只需要查找服务并获得接口实例,服务器端只需要编写实现类并注册为服务;
RMI的序列化和反序列化可能会造成安全漏洞,因此调用双方必须是内网互相信任的机器,不要把1099端口暴露在公网上作为对外服务。