Java网络编程简明教程
计算机网络相关概念
计算机网络是两台或更多的计算机组成的网络,同一网络内的任意两台计算机可以直接通信,所有计算机必须遵循同一种网络协议。
-
互联网
- 互联网是连接计算机网络的网络
- 互联网采取TCP/IP协议
- 其中最重要的两个协议是TCP协议和IP协议
-
IP地址和网关
-
IP地址用于唯一标识一个网络接口
-
IPv4采用32位地址
IPv4地址实际是一个二进制32位的整数,为了便于识别,用十六进制表示后可以分为4组数字,每组数字转换成十进制后用“.”隔开就是我们见到的IP地址:
-
IPv6采用128位地址
-
公网IP地址可以直接被访问
-
内网IP地址只能在内网访问
-
本机地址使用127.0.0.1
-
通常路由器或交换机有两个网卡(两个IP地址),分别连接两个不同的网络:
-
同一网络下的计算机可以直接通信,他们的网络号相同,网络号由IP地址和子掩码按组对齐做与运算得到:
-
不同网络下的计算机需要通过路由器或交换机网络设备间接通信,这样的网络设备叫做网关:
-
网关的作用是连接多个网络,负责把一个网络的数据包发送到另一个网络,过程叫做路由:
-
一台计算机的网络拥有IP地址,子网掩码和网关(路由器)三个关键配置:
-
-
域名
由于IP地址不便于记忆,通常使用域名来访问特定的服务,域名解析服务器DNS负责将域名翻译成对应的IP地址,客户端再根据IP地址访问服务器:
-
TCP/IP协议
- IP协议是一个分组交换协议,不保证可靠传输,一个数据包通过IP协议传输会自动分成若干小的数据包然后通过网络进行传输
- TCP(Transmission Control Protocol)协议是一个传输控制协议,建立再IP协议之上,IP协议负责传输数据包,TCP协议负责控制传输数据包;TCP协议传输之前需要先建立连接,然后才能传输数据,传输完成后断开连接;TCP协议是一个可靠传输协议,他通过接收确认,超时重传实现;TCP协议支持双向通信,双方可以同时传输和接收数据
-
UDP协议
UDP(User Datagram Protocol)协议是数据报文协议,不面向连接,不保证可靠传输,由于UDP协议传输效率高,通常用来传输视频等能容忍丢失部分数据的文件。
-
Socket
Socket通常称为套接字,用于应用程序之间建立远程连接,Socket内部通过TCP/IP协议进行数据传输,可以简单的理解为对IP地址和端口号的描述。Socket接口是由计算机操作系统提供的,编程语言提供对Socket接口调用的封装。通常计算机同时运行多个应用程序,仅仅有IP地址是无法确定由哪个应用程序接收数据,所以操作系统抽象出Socket接口,每个应用程序对应不同的socket(每个网络应用程序分配不同的端口号)。端口号的范围是0~65535,小于1024的端口需要管理员权限,大于1024的端口可以任意用户的应用程序打开。
Socket编程需要实现服务器端和客户端,因为这两个设备通讯时,需要知道对方的IP和端口号。通常服务器端有个固定的端口号,客户端直接通过服务器的IP地址和端口号进行访问服务器端,同时告知客户端的端口号,于是他们之间就可以通过socket进行通信。
TCP编程
- TCP客户端
Java提供了Socket类ServerSocket类对计算机操作系统的Socket进行调用。客户端使用Socket(InetAddress, port)构造方法传入IP地址和端口号打开Socket,与远程服务区指定端口进行连接, 然后调用socket的getInputStream和getOutputStream方法获取输入和输出流就可以读写TCP的字节流:
// 连接远程服务器
Socket socket = new Socket(InetAddress, port);
// 读写字节流
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
- TCP服务端
服务器端通过ServerSocket(port)构造方法传入端口号来监听指定的端口,然后通过accept()方法得到一个Socket对象与远程客户端建立连接,同样调用Socket对象的getInputStream和getOutputStream方法就可以读写字节流,服务器端完成传输后可以通过close()方法关闭远程连接和监听端口:
// 监听端口
ServerSocket serverSocket = new ServerSocket(port);
// 建立远程连接
Socket socket = serverSocket.accept();
// 读写字节流
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
// 关闭连接
socket.close();
// 关闭监听端口
serverSocket.close();
- TCP编程实验
我们可以在本机做一个小实验,首先编写一个客户端的TCPClient类,通过Java提供的InetAddress类的getLoopbackAddress()方法获得localhost地址,然后使用Java的Socket类创建一个与本机8090端口的连接,再将读取字节流包装成一个BufferedReader对象、写入字节流包装成BufferedWriter对象。使用BufferedWriter写入一个“time”字符串并发送到本机的8090端口,再用BufferedReader读取本机8090端口返回的数据并打印出来。代码如下:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class TCPClient {
public static void main(String[] args) throws IOException {
// 获取本机地址,即“127.0.0.1”
InetAddress addr = InetAddress.getLoopbackAddress();
// 与本机8090端口建立连接
try (Socket sock = new Socket(addr, 8090)) {
// 将读写字节流包装成BufferedReader和BufferedWriter对象
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(sock.getInputStream(), StandardCharsets.UTF_8))) {
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.UTF_8))) {
// 写入“time”字符串
writer.write("time\n");
// 将写入内存缓冲区的数据立即发送
writer.flush();
// 读取本机8090端口返回的数据
String resp = reader.readLine();
System.out.println("Response: " + resp);
}
}
}
}
}
在相同包下写一个服务端的TCPServer类,利用Java的ServerSocket类监听8090端口并打印一句话“TCP server ready.”,然后用ServerSocket类的accept()方法与监听到的访问8090端口的客户端请求建立连接,然后和客户端一样包装读写字节流。服务端首先读取数据,如果读取到的数据是一个"time"字符串,则将当前时间信息返回给客户端,如果不是则返回一个“require data”字符串给客户端,最后关闭连接和关闭监听接口。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
public class TCPServer {
public LocalDateTime currentTime() {
return LocalDateTime.now();
}
public static void main(String[] args) throws Exception {
// 监听8090端口
ServerSocket ss = new ServerSocket(8090);
System.out.println("TCP server ready.");
// 建立连接
Socket sock = ss.accept();
// 包装读写字节流
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(sock.getInputStream(), StandardCharsets.UTF_8))) {
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.UTF_8))) {
// 读取发送到服务端的数据
String cmd = reader.readLine();
// 如果数据是“time”字符串则将当前时间信息返回客户端
if ("time".equals(cmd)) {
writer.write(LocalDateTime.now().toString() + "\n");
// 将写入内存缓冲区的数据立即发送
writer.flush();
} else {
writer.write("require data\n");
writer.flush();
}
}
}
// 关闭连接
sock.close();
// 关闭监听端口
ss.close();
}
}
我们首先运行服务端TCPServer类的main方法,开始监听8090端口,并且Console打印出“TCP server ready.”,然后运行客户端TCPClient的main方法,我们得到Response信息,终端打印出了当前的时间信息:
如果我们先运行客户端的main方法,我们会得到一个异常ConnectException: Connection refused,因为服务端并没有开始监听8090端口,无法与客户端建立socket连接。
- TCP多线程
服务端的一个ServerSocket可以同时和多个客户端建立连接进行双向通信,实现起来也很简单,在设置好监听端口后,在一个无限for循环中调用ServerSocket的accept()方法,返回与客户端新建的连接,再启动线程或者使用线程池来处理客户端的请求,这样就可以同时处理多个客户端的连接,代码如下:
public class TCPServer {
public static void main(String[] args) throws Exception {
@SuppressWarnings("resource")
ServerSocket ss = new ServerSocket(8090);
System.out.println("TCP server ready.");
for (;;) { // 无限for循环中返回客户端新建的连接
Socket sock = ss.accept();
// 设置线程要处理的任务
Runnable handler = new TimeHandler(sock);
// 使用Java提供的ExecutorService创建线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 线程处理任务
executor.submit(handler);
// 任务处理完毕,关闭线程
executor.shutdown();
}
}
}
class TimeHandler implements Runnable {
Socket sock;
TimeHandler(Socket sock) {
this.sock = sock;
}
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(sock.getInputStream(), StandardCharsets.UTF_8))) {
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.UTF_8))) {
for (;;) {
String cmd = reader.readLine();
if ("q".equals(cmd)) {
writer.write("bye!\n");
writer.flush();
break;
} else if ("time".equals(cmd)) {
writer.write(LocalDateTime.now().toString() + "\n");
writer.flush();
} else {
writer.write("require data\n");
writer.flush();
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
this.sock.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
UDP编程
UDP协议不需要建立连接,可以直接发送和接收数据,UDP协议不保证可靠传输,使用Java提供的DatagramSocket的send()和receive()方法就可以发送和接收数据,UDP协议接收和发送数据没有IO流接口。
- 客户端
public class UDPClient {
public static void main(String[] args) throws Exception {
InetAddress addr = InetAddress.getLoopbackAddress();
try (DatagramSocket sock = new DatagramSocket()) { // 创建DatagramSocket对象
sock.connect(addr, 9090); // 设置要访问的服务端地址和端口
byte[] data = "time".getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(data, data.length); // 将字节流包装成DatagramPacket对象
sock.send(packet); // 发送数据
System.out.println("Data was sent.");
Thread.sleep(1000);
byte[] buffer = new byte[1024];
DatagramPacket resp = new DatagramPacket(buffer, buffer.length);
sock.receive(resp); // 接收数据
byte[] respData = resp.getData();
String respText = new String(respData, 0, resp.getLength(), StandardCharsets.UTF_8);
System.out.println("Response: " + respText);
}
}
}
- 服务端
public class UDPServer {
public LocalDateTime currentTime() {
return LocalDateTime.now();
}
public static void main(String[] args) throws Exception {
@SuppressWarnings("resource")
DatagramSocket ds = new DatagramSocket(9090); // 设置要监听的端口
System.out.println("UDP server ready.");
for (;;) {
// 接收数据:
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
byte[] data = packet.getData();
String s = new String(data, StandardCharsets.UTF_8);
System.out.println("Packet received from: " + packet.getSocketAddress() + " " + s);
// 发送数据:
String resp = LocalDateTime.now().toString();
packet.setData(resp.getBytes(StandardCharsets.UTF_8));
ds.send(packet);
}
}
}
Email编程
电子邮件的一些基本概念:
- MUA(Mail User Agent)发送和接收邮件所使用的邮件客户端,通常使用IMAP或POP3协议与服务器通信
- MTA(Mail Transfer Agent) 通过SMTP协议发送、转发邮件
- MDA(Mail Deliver Agent)将MTA接收到的邮件保存到磁盘或指定地方
- SMTP协议(Simple Mail Transfer Protocol)是发送邮件使用的标准协议,建立在TCP协议之上,标准端口25,加密端口为465/587
- POP3协议(Post Office Protocol version3) 是接收邮件使用的标准协议之一,建立在TCP协议之上,标准端口为110,加密端口为995
- IMAP协议 (Internet Mail Access Protocol )是接收邮件使用的标准协议之一,和POP3协议的区别是IMAP协议允许用户创建文件夹,用户在本地的任何操作都自动同步到邮件服务器,准备端口为143,加密端口为993
Java提供了一个javax.mail包,可以很方便的实现发送和接收邮件,而不用去关系SMTP协议和POP3协议的原理,方法如下:
- 发送邮件
-
首先创建一个Session对象,传入邮件服务器信息和用户名密码认证信息
Session session = Session.getInstance(props, new Authticator());
-
然后创建MimeMessage对象,封装邮件发件人地址、收件人地址,邮件主题,邮件正文等信息
MimeMessage message = new MimeMessage(session); message.setFrom(new InternetAddress("from@email.com")); message.setRecipient(Message.RecipientType.TO, new InternetAddress("to@email.com")); message.setSubject("subject", "UTF-8"); message.setText("body", "UTF-8");
-
最后使用Transport工具类的send()方法发送邮件
Transport.send(message);
-
- 接收邮件
-
首先创建Session对象,同样需要传入服务器信息和登录名密码认证
Session session = Session.getInstance(props, null);
-
然后创建Store对象,Store对象代表用户在服务器上的整个存储
Store store = new POP3SSLStore(session, url)
-
通过Store对象获取Folder对象,获取用户相应的文件夹,比如收件箱“INBOX”
Folder folder = store.getFolder("INBOX");
-
从Folder对象中获取所有的邮件
Message[] messages = folder.getMessage(); for (Message message : messages) { ... };
-
由于本文只是Java编程简明教程,对Email编程就不作过多的讲述,更多的功能可以参考JAVA MAIL相关API文档。
HTTP编程
HTTP协议(HyperText Transfer Protocol)又叫做超文本传输协议,它是基于TCP协议上的请求和响应协议,是目前使用最广泛的高级协议。最早的HTTP协议版本是HTTP 1.0,每一次请求,都会创建一个TCP连接,由于浏览器打开网页通常会请求不同的资源(例如图片,CSS等其他资源),创建TCP连接会有一定耗时,所以传输效率比较低;HTTP 1.1 则做出改进,多个HTTP请求可以通过一个TCP连接完成,效率得到提高;HTTP 2.0 同样也是多个请求通过一个TCP连接完成,但是浏览器发送一个请求后不需要等待服务器的响应就可以立刻发送后续的请求,服务器只要有了响应数据立刻返回,不关心请求的顺序,也就是说HTTP 2.0 不需要严格按照收到请求再响应的方式进行。
HTTP服务器用于处理HTTP请求,发送HTTP响应。在Java中,HTTP服务器通过JAVA EE的Servlet API定义,通常Servlet容器根据收到的HTTP请求信息创建一个Request对象,再创建一个Response对象用来向Web客户端发送响应,调用Servlet对象的service()方法处理Request和Response,具体参考Servlet教程。
HTTP客户端用于发送HTTP请求,接收HTTP响应。Java提供的java.net.HttpURLConnection类可以实现HTTP客户端功能:
- 发送GET请求
URL url = new URL("http://www.example.com/");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
int code = conn.getResponseCode(); //获得响应代码
try (InputStream input = conn.getInputStream()) {
// 读取响应
}
conn.disconnect(); // 断开连接
- 发送POST请求
URL url = new URL("http://www.example.com/");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true); // 需要发送请求数据
byte[] postData = contentData.getBytes(StandardCharsets.UTF_8); // 将发送请求数据转换为数组
conn.setRequestProperty("Content-Type", contentType); // 设置请求Content-Type
conn.setRequestProperty("Content-Length", String.valueOf(postData.length)); 设置请求Content-Length
try (OutputStream output = conn.getOutputStream()) {
output.write(postData); // 写入请求数据
}
int code = conn.getResponseCode(); // 获取响应代码
try (InputStream input = conn.getInputStream()) {
// 读取响应数据
}
conn.disconnect(); // 断开连接
总结
Java提供了Socket和ServerSocket类,让我们可以实现TCP/UDP协议的连接;Java还提供了MAIL API,我们可以实现基于SMTP/POP3协议的收发邮件功能;最后Java还提供了HttpURLConnection类,用于实现HTTP客户端功能,以及提供了Servlet API用于实现HTTP服务端的编程。