本文整理了学习Java网络通信编程的笔记,并分析了若干程序实例,以巩固学习成果。
1 网络通信编程笔记
1.1 网络程序设计基础
1.1.1 基本概念
- 局域网(LAN)、广域网(WAN)
- IP协议,IP地址(IPv4,4个字节表示)
- TCP协议(传输控制协议):类似拨打电话,固接连线,可靠性高,有顺序
- UDP协议(数据用户报协议):类似发送信件,无连接通信,可靠性低,不保证顺序
- 端口(port):假想的连接装置,计算机与网络的物理连接,为整数
- 套接字(Socket):假想的连接装置,连接程序与端口
1.1.2 网络通信的要素
- 通信双方地址:IP、端口号
- 网络通信协议:TCP/IP协议
1.2 TCP程序设计基础
1.2.1 InetAddress 类
与IP地址相关的类,注意该类会抛UnknownHostException 异常
- IP地址:
- 本机
localhost (127.0.0.1) - IPv4(4个字节组成,42亿),IPv6(128位,8个无符16进制整数)
- 公网(互联网),私网(局域网),ABCD类地址
- 无构造器,不可被
new ,只可被自己的方法返回 - 常用方法:
getByName(String host) 获取与Host对应的InetAddress 对象getHostAddress() 获取对象所包含的IP地址,返回StringgetHostName() 获取IP主机名,返回StringgetLocalHost() 获取本地主机的InetAddress 对象
1.2.2 ServerSocket 类
服务器套接字,等待网络请求,注意该类会抛IOException 异常
- 端口:
- 0~65535
- 不同的进程用不同的端口号,类似于门牌
- 端口分类:
- 公有端口0~1023(
HTTP 80、HTTPS 443、FTP 21、Telent 23) - 程序注册端口1024~49151(
Tomcat 8080、MySQL 3306、Oracle 1521) - 动态(私有)端口49152~65535
InetSocketAddress 类:与InetAddress 类似,加入了端口,可以new ,传入String地址和int端口,有getPort() 等方法ServerSocket 用于等待网络请求,构造方法:
ServerSocket() 非绑定服务器套接字ServerSocket(int port) 绑定特定端口ServerSocket(int port, int backlog) 指定本机端口、指定的backlog ServerSocket(int port, int backlog, InetAddress bindAddress) 指定端口、侦听backlog 和绑定到的本地IP地址 ServerSocket 的常用方法:
accept() 等待客户机连接,若连接返回一个Socket套接字isBound() 判断绑定状态getInetAddress() 返回本地地址的InetAddress isClosed() 返回关闭状态close() 关闭服务器套接字bind(SocketAddress endpoint) 绑定到特定地址(IP和端口)getLocalPort() 获取等待端口
1.2.3 TCP网络程序
-
通信协议:速率、传输码率、代码结构、传输控制等
- TCP/IP协议:协议簇,最出名的是TCP协议和IP协议
- TCP:连接、稳定,三次握手四次挥手,客户端服务端架构,传输完成释放连接,效率低
- UDP:不连接、不稳定,客户端服务端无明确界限,效率高
-
参见第2章的实例
1.2.4 Tomcat基础
- Tomcat是一个服务端,客户端通过浏览器进入
- 一般使用8080端口
1.4 UDP程序设计基础
1.4.1 UDP通信
- 基本模式:
- 发送数据包步骤:
- 创建数据报套接字(
DatagramSocket() ) - 创建发送的数据包(
DatagramPacket(byte[] buf, int offset, int length, InetAddress ip, int port) ) - 发送数据包(
DatagramSocket().send() ) - 接收数据包步骤:
- 创建数据报套接字并绑定到端口(
DatagramSocket(int port) ) - 创建字节数组接收数据包(
DatagramPacket(byte[] buf, int length) ) - 接收UDP包(
DatagramSocket().receive() )
1.4.2 DatagramPacket 类
- 表示数据包
- 构造方法:
DatagramPacket(byte[] buf, int length) 指定数据包的内存空间和大小DatagramPacket(byte[] buf, int length, InetAddress ip, int port) 指定数据包的目标地址和端口
1.4.3 DatagramSocket 类
- 表示发送和接收数据包的数据报套接字
- 构造方法:
DatagramSocket() 绑定到本地主机任何可用端口DatagramSocket(int port) 绑定到本地主机指定端口DatagramSocket(int port, InetAddress ip) 绑定到指定的本地地址
1.4.4 UDP网络程序
1.5 URL类
统一资源定位器,通过地址定位互联网上的资源
2 TCP网络程序示例
2.1 简单的接收器(服务端)、发送器(客户端)程序
接收器(MyReceiver.java )
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class MyReceiver {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
serverSocket = new ServerSocket(9005);
socket = serverSocket.accept();
is = socket.getInputStream();
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println(baos);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
发送器(MyTransmitter.java )
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
public class MyTransmitter {
public static void main(String[] args) {
Socket socket = null;
OutputStream os = null;
try {
InetAddress serverIP = InetAddress.getByName("127.0.0.1");
int port = 9005;
socket = new Socket(serverIP, port);
os = socket.getOutputStream();
os.write("你好!".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
说明:
- 服务端和客户端都需要有套接字,分别是
ServerSocket 和Socket ,并且绑定到统一端口。服务端还有一个Socket ,是连接端口的返回结果。 - 注意输入流和输出流的使用,客户端使用输出流,通过套接字输出;服务端使用输入流,通过套接字输入,而这股输入流需要输出的话,还需要一个输出流。
先启动MyReceiver.java ,再启动MyTransmitter.java ,观察到MyReceiver.java 输出:
你好!
2.2 较复杂的服务端、客户端程序
服务端(MyTCPServer.java )
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class MyTCPServer {
private ServerSocket server;
private Socket socket;
private BufferedReader reader;
public static void main(String[] args) {
MyTCPServer tcp = new MyTCPServer();
tcp.getServer();
}
void getServer() {
try {
server = new ServerSocket(8998);
System.out.println("服务器套接字创建成功!");
while (true) {
System.out.println("等待客户端连接...");
socket = server.accept();
System.out.println("客户端连接成功!");
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
getClientMessage();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void getClientMessage() {
try {
while (true) {
System.out.println("客户端:" + reader.readLine());
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (reader != null) {
reader.close();
}
if (socket != null) {
socket.close();
}
if (server != null) {
server.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端(MyTCPClient.java )
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
public class MyTCPClient extends JFrame {
Container c;
private final JTextArea ta = new JTextArea();
private final JTextField tf = new JTextField();
Socket socket;
private PrintWriter writer;
public MyTCPClient(String title) {
super(title);
setDefaultCloseOperation(EXIT_ON_CLOSE);
c = getContentPane();
final JScrollPane sp = new JScrollPane();
sp.setBorder(new BevelBorder(BevelBorder.RAISED));
c.add(sp, BorderLayout.CENTER);
sp.setViewportView(ta);
ta.setFont(new Font("微软雅黑", Font.PLAIN, 16));
ta.setEditable(false);
c.add(tf, BorderLayout.SOUTH);
tf.setFont(new Font("微软雅黑", Font.PLAIN, 16));
tf.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
writer.println(tf.getText());
ta.append(tf.getText() + '\n');
ta.setSelectionEnd(ta.getText().length());
tf.setText("");
}
});
}
public static void main(String[] args) {
MyTCPClient client = new MyTCPClient("客户端系统");
client.setBounds(600, 300, 400, 400);
client.setVisible(true);
client.connect();
}
private void connect() {
ta.append("尝试连接中...\n");
try {
socket = new Socket("127.0.0.1", 8998);
writer = new PrintWriter(socket.getOutputStream(), true);
ta.append("完成连接!\n");
} catch (IOException e) {
ta.append("连接失败!请检查服务器端\n");
}
}
}
说明:
- 服务端将获取客户端的方法放在无限循环中,以便无限接收客户端的信息。
- 根据屏幕提示的信息,确定客户端是何时连接上服务端的。
先启动MyTCPServer.java ,再启动MyTCPClient.java ,在窗体输入文字,观察到窗体变化与MyTCPServer.java 输出:
服务器套接字创建成功!
等待客户端连接...
客户端连接成功!
客户端:你好!
客户端:我是客户端
2.3 简单的文件传输程序
服务端(FileReceiver.java )
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class FileReceiver {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(9000);
Socket socket = server.accept();
InputStream is = socket.getInputStream();
FileOutputStream fos = new FileOutputStream("receive.png");
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
BufferedWriter endBw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
endBw.write("接收完毕");
endBw.close();
fos.close();
is.close();
socket.close();
server.close();
}
}
客户端(FileSender.png )
import java.io.*;
import java.net.Socket;
public class FileSender {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 9000);
OutputStream os = socket.getOutputStream();
FileInputStream fis = new FileInputStream("send.png");
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
socket.shutdownOutput();
BufferedReader endBr = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println(endBr.readLine());
endBr.close();
fis.close();
os.close();
socket.close();
}
}
说明:
- 示例中涉及到的输入输出流分别有(以输入流为例):
InputStream (接收套接字流)、FileInputStream (用于文件与系统间的传输流)、InputStreamReader (管道)、BufferedReader (用于处理服务端的回传字符)。 - 注意客户端第15行的
socket.shutdownOutput(); ,若不写这一行,即使客户端已写入完毕(并开始进行输入流的等待),服务端仍在继续读取(因为服务端第13行的is.read(buffer) ,读取的字节数已为0,因此循环无法退出,处于读取0字节写入0字节的状态),因此要单向关闭客户端的套接字输出流,保证服务端结束读取。
先启动FileReceiver.java ,再启动FileSender.java ,观察到文件receive.png 生成,以及FileSender.java 输出:
接收完毕
3 UDP网络程序示例
3.1 简单的发送器、接收器程序
发送器(MyUDPSender.java )
import java.net.*;
public class MyUDPSender {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket();
String msg = "你好!";
byte[] buffer = msg.getBytes();
InetAddress localhost = InetAddress.getByName("localhost");
int port = 9090;
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length, localhost, port);
socket.send(packet);
socket.close();
}
}
接收器(MyUDPReceiver.java )
import java.net.*;
public class MyUDPReceiver {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(9090);
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
socket.receive(packet);
System.out.println(packet.getAddress().getHostAddress());
System.out.println(new String(packet.getData(), 0, packet.getLength()));
socket.close();
}
}
说明:
- 发送器和接收器都有数据报套接字
DatagramSocket 和数据包DatagramPacket ,但使用方式不同。
- 发送器的
DatagramSocket 不用绑定端口,因为它只用send() 方法,无需绑定发送者的端口;接收器的DatagramSocket 需要绑定端口,因为它用receive() 方法需要明确自己的地址。 - 发送器的
DatagramPacket 需要绑定发送地址,因为它已有内容并打包好,需要向特定地址传递;接收器的DatagramPacket 不用绑定地址,因为它是一个空的容器,只起接收载体作用。 - 接收器第10行,
packet.getLength() 不能用packet.getData().length 代替,数据包大小与数据字节数组的大小是不同的。
先启动MyUDPReceiver.java ,观察到程序开始等待,再启动MyUDPSender.java ,观察到MyUDPReceiver.java 输出:
127.0.0.1
你好!
3.2 较复杂的聊天器程序(多线程)
聊天发送端线程(MyChatSender.java )
import java.io.*;
import java.net.*;
public class MyChatSender implements Runnable {
DatagramSocket socket = null;
BufferedReader reader = null;
private final String toIP;
private final int toPort;
public MyChatSender(String toIP, int toPort) {
this.toIP = toIP;
this.toPort = toPort;
try {
socket = new DatagramSocket();
reader = new BufferedReader(new InputStreamReader(System.in));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
try {
String data = reader.readLine();
byte[] dataBytes = data.getBytes();
DatagramPacket packet = new DatagramPacket(dataBytes, 0, dataBytes.length, new InetSocketAddress(toIP, toPort));
socket.send(packet);
if ("bye".equals(data)) {
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
socket.close();
}
}
聊天接收端线程(MyChatReceiver.java )
import java.io.*;
import java.net.*;
public class MyChatReceiver implements Runnable {
DatagramSocket socket = null;
private final String fromName;
public MyChatReceiver(int port, String fromName) {
this.fromName = fromName;
try {
socket = new DatagramSocket(port);
} catch (SocketException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
try {
byte[] container = new byte[1024];
DatagramPacket packet = new DatagramPacket(container, 0, container.length);
socket.receive(packet);
byte[] dataBytes = packet.getData();
String data = new String(dataBytes, 0, packet.getLength());
System.out.println(fromName + ":" + data);
if ("bye".equals(data)) {
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
socket.close();
}
}
聊天者A(ChatterA.java )
public class ChatterA {
public static void main(String[] args) {
new Thread(new MyChatSender("localhost", 9999)).start();
new Thread(new MyChatReceiver(8888, "B")).start();
}
}
聊天者B(ChatterB.java )
public class ChatterB {
public static void main(String[] args) {
new Thread(new MyChatSender("localhost", 8888)).start();
new Thread(new MyChatReceiver(9999, "A")).start();
}
}
说明:每个聊天者有两个线程,以及两个所需端口。A向localhost 的9999端口发信息,同时接收发送到8888端口名为B发来的信息;B向localhost 的8888端口发信息,同时接收发送到9999端口名为A发来的信息。
分别启动ChatterA.java 和ChatterB.java ,并输入文字,观察到两个程序分别输出:
你好!
B:你好啊!
今天天气真不错。
B:是啊,今天天气晴朗。
bye
B:bye
A:你好!
你好啊!
A:今天天气真不错。
是啊,今天天气晴朗。
A:bye
bye
3.3 较复杂的广播、收音机程序
广播(MyBroadcast.java )
import java.net.*;
public class MyBroadcast extends Thread {
String msg = "欢迎收听广播节目。";
int port = 9898;
InetAddress ipGroup;
MulticastSocket socket;
MyBroadcast() throws Exception {
ipGroup = InetAddress.getByName("224.255.10.0");
socket = new MulticastSocket(port);
socket.setTimeToLive(1);
socket.joinGroup(ipGroup);
}
@Override
public void run() {
while (true) {
byte[] data = msg.getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, ipGroup, port);
System.out.println(new String(data));
try {
socket.send(packet);
sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws Exception {
MyBroadcast broadcast = new MyBroadcast();
broadcast.start();
}
}
收音机(MyRadio.java )
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
public class MyRadio extends JFrame implements Runnable, ActionListener {
int port;
InetAddress ipGroup;
MulticastSocket socket;
JButton start = new JButton("开始接收");
JButton stop = new JButton("停止接收");
JTextArea startArea = new JTextArea(10, 10);
JTextArea receivedArea = new JTextArea(10, 10);
Thread thread;
boolean isStopped = false;
public MyRadio() throws Exception {
super("数据报收音机");
setDefaultCloseOperation(EXIT_ON_CLOSE);
thread = new Thread(this);
start.addActionListener(this);
stop.addActionListener(this);
startArea.setForeground(Color.BLUE);
JPanel north = new JPanel();
north.add(start);
north.add(stop);
add(north, BorderLayout.NORTH);
JPanel center = new JPanel(new GridLayout(1, 2));
center.add(startArea);
center.add(receivedArea);
add(center, BorderLayout.CENTER);
validate();
port = 9898;
ipGroup = InetAddress.getByName("224.255.10.0");
socket = new MulticastSocket(port);
socket.joinGroup(ipGroup);
setBounds(100, 50, 360, 380);
setVisible(true);
}
@Override
public void run() {
while (true) {
byte[] data = new byte[1024];
DatagramPacket packet = new DatagramPacket(data, data.length, ipGroup, port);
try {
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength());
startArea.setText("正在接收的内容:\n" + msg);
receivedArea.append(msg + "\n");
} catch (Exception e) {
e.printStackTrace();
}
if (isStopped) {
break;
}
}
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == start) {
start.setBackground(Color.RED);
stop.setBackground(Color.YELLOW);
if (!(thread.isAlive())) {
thread = new Thread(this);
}
thread.start();
isStopped = false;
}
if (e.getSource() == stop) {
start.setBackground(Color.YELLOW);
stop.setBackground(Color.RED);
isStopped = true;
}
}
public static void main(String[] args) throws Exception {
MyRadio radio = new MyRadio();
radio.setSize(460, 200);
}
}
说明:
- 发出广播和接收广播的地址必须位于同一个组内,地址范围是224.0.0.0~224.255.255.255,该地址不代表某个特定主机的位置。
- 加入同一个组的主机可以在某个端口广播信息,也可以在某个端口接收信息。
启动MyBroadcast.java ,观察到其开始输出;启动MyRadio.java 并点击开始接收按钮,观察到其开始接收广播信息:
欢迎收听广播节目。
欢迎收听广播节目。
欢迎收听广播节目。
4 利用URL下载网络资源示例
import javax.net.ssl.HttpsURLConnection;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
public class Main {
public static void main(String[] args) throws Exception {
URL url = new URL("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
InputStream is = connection.getInputStream();
FileOutputStream fos = new FileOutputStream("logo.png");
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.close();
is.close();
connection.disconnect();
}
}
观察到文件logo.png 的生成。
5 I/O补充
使用输入流、输出流时以下这段代码的原理:
FileInputStream fis = new FileInputStream("xxx");
OutputStream os = new OutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
buffer 是一个字节数组,长度为1024,类似于一个缓冲区。fis.read(buffer) ,这一步从输入流读取buffer 大小的字节(即1024),把这些字节赋给buffer ,并返回读取的字节数。- 把读取的字节数赋值给
len ,然后对len 进行判断。 - 把
buffer 数组的0到len 位置的字节写入输出流。 - 每次循环,
buffer 就被重新赋值一次,因此文件大小与buffer 的长度1024是没有关系的。 - 读取到输入流末尾时,
read() 方法返回-1,循环结束。 - 注意:缓冲区的大小不应太小,否则会涉及到编码问题,部分字节被强行拆分到两个缓冲区时可能会出现乱码。
|