网络编程
本文章为个人“Java零基础实战”学习笔记,侵权删
所谓的 Web 编程就是编写程序运行在同一网络下的两个终端上,使得他们之间可以进行数据传输。在正式学习 Java Web 编程之前,我么先来了解网络的相关基础知识。
计算机网络就是通过硬件设施、传输媒介把各个不同的物理地址上的计算机进行连接,形成一个资源共享和数据传输的网络系统。两台终端通过网络进行连接时,需要遵守一定的规则,这个规则就是网络协议(network rotocol),网络协议主要由 3 个特征组成。
- 语法:数据信息的结果。
- 语义:描述请求、动作和响应。
- 同步:动作的实现顺序。
网络通信协议有 TCP/IP 协议、IPX/SPX 协议、NetBEUI 协议等,我们常用的是 TCP/IP 协议,同时 TCP/IP 协议是分层的,分层的优点如下:
- 各层之间相互独立,互不干扰;
- 维护性,扩展性好;
- 有利于系统的标准化。
分层的思想在程序开发中的应用非常普遍,它的好处是每一层只关注自己的业务,无需去关注其他层的业务,只需要获取其他层传来的信息,进行处理之后再传给下一层。例如我们在编写 Java 代码的时候,不需要考虑底层的操作系统是 Windows 还是 Mac。分层思想已经帮我们屏蔽了底层机制,我们只需要在应用层写业务代码即可。TCP/IP 协议可以分为四层,从上到下依次分为应用层、传输层、网络层和网络接口层。
- 应用层(application layer)是整个体系结构中的顶层,通过应用程序之间的数据交互完成网络应用。
- 传输层(transport layer)为两台终端中应用程序之间的数据交互提供数据传输服务。
- 网络层(network layer)也叫 IP 层,负责为网络中不同的终端提供信息服务。
- 网络接口层(network interface layer)包括数据链路层(data link layer)和物理层(physical layer),数据链路层的作用是为两台终端的数据传输提供链路协议;物理层是指光纤、电缆或者电磁波等正式存在的物理媒介,这些媒介可以传送网络信号。
终端 A 正在和终端 B 通过网络进行通信,整个数据的传输流程是终端 A -> 应用层 -> 传输层 -> 网络层 -> 数据链路层 -> 网络层 -> 传输层 -> 应用层 -> 终端 B
IP 与端口
IP
互联网中的每台终端设备都有一个唯一表示,网络中的请求可以根据这个标识找到具体的计算机,这个唯一表示就是 IP 地址(Internet Protocol),用户可以通过操作系统的设置来查看本机的 IP 地址。IP 地址是 32 位的二进制数据,但是我们所看到的 IP 地址已经转为了十进制数据。
IP 地址 = {<网络地址>, <主机地址>}, 网络地址的作用是找到主机所在的网络,主机地址的作用是找到网络中的主机。IP 地址分为 5 类,各类地址可使用的 IP数量不同,棘突范围如表:
分类 | 范围 |
---|---|
A 类 | 1.0.0.1 ~ 126.255.255.254 |
B 类 | 128.0.0.1 ~ 191.255.255.254 |
C 类 | 192.0.0.1 ~ 223.225.225.254 |
D 类 | 224.0.0.1 ~ 239.255.255.254 |
E 类 | 240.0.0.1 ~ 255.255.255.254 |
需要注意的是我们在实际开发中斌不需要 记住本机的 IP 地址,可以使用 127.0.0.1 或者 localhost 来表示本机 IP 地址, Java 中有专门的类来描述 IP 地址,这个类是 java.net.InetAddress,该类的常用方法如表:
方法 | 描述 |
---|---|
public static InetAddress getLocalHost() throws UnknownHostException | 获取本地主机的 InetAddress 对象 |
public static InetAddress getByName(String host) throws UnknownHostException | 通过主机名称创建 InetAddress 对象 |
String getHostName() | 获取主机名称 |
public String getHostAddress() | 获取主机 IP 地址 |
public static InetAddress getByAddress(String host, byte[] addr) throws UnknownHostException | 通过主机名称和 IP 地址创建 InetAddress 对象 |
public static InetAddress getByAddress(byte[] addr) throws UnknownHostException | 通过 IP 地址创建 InetAddress 对象 |
public class TestIp {
public static void main(String[] args) {
try {
InetAddress inetAddress = InetAddress.getLocalHost();
System.out.println(inetAddress.getHostName());
System.out.println(inetAddress.getHostAddress());
InetAddress inetAddress2 = InetAddress.getByName("127.0.0.1");
System.out.println(inetAddress2);
inetAddress2 = InetAddress.getByName("localhost");
System.out.println(inetAddress2);
} catch (UnknownHostException e) {
// TODO: handle exception
}
}
}
运行结果:
LAPTOP-N9OVN3H4
169.254.64.126
/127.0.0.1
localhost/127.0.0.1
端口
如果把 IP 比作一栋大厦的地址,那么端口(port)就是不同房间的门牌号。IP 地址需要和端口结合起来使用,好比快递小哥必须要通过大厦地址和房间号才能准确找到你。计算机主机就相当于大厦,网络中的请求需要通过 IP 地址来找到主机,同时一台主机上会同时运行很多个服务,如何把不同的请求分配给不同的服务就需要使用到端口了。
例如你的计算机同时打开了微信和QQ,这是一台主机上的两个服务,朋友通过 QQ 给你发送了一条信息,那么为什么请求来到主机被QQ服务所接收到而不是微信呢?就是应为不同的服务会有不同的端口,请求根据 “IP 地址+端口” 就可以准确地找到互联网中的接收它的服务了。比如要连接本地的MySQL 数据库服务,MySQL 数据库服务的端口是 3306,那么完整的 URL 请求就是 localhost:3306。
URL 和 URLConnection
URL
通常讲的网络资源实际是指网络中真实存在的一个实体,比如文字、图片、视频、音频等,如果我们要在程序获取网络实体,应该怎么做呢?可以用 URI (Uniform Resource Identifier)统一资源定位符指向目标实体,URI 的作用是用特定的语法来标识某个网络资源。 Java 中专门封装了一个类用来描述 URI,这个类就是 java.net.URI,使用 URI 的实例化对象,可以用面向对象的方式来管理网络资源,如获取主机地址、端口等,在本机(127.0.0.1)上部署服务(web_need),通过 URI 读取该服务的 login.jsp 资源的具体操作如下:
public class TestURI {
public static void main(String[] args) {
try {
URI uri = new URI("http://localhost:8080/web_need/login.jsp");
System.out.println(uri.getHost());
System.out.println(uri.getPort());
System.out.println(uri.getPath());
} catch (URISyntaxException e) {
// TODO: handle exception
}
}
}
运行结果:
localhost
8080
/web_need/login.jsp
那什么是 URL 呢?URL 和 URI 有什么区别呢?URL(Uniform Resource Locator)是统一资源位置,在 URI 的基础上进行了扩充,在定位资源的同时还提供了对应的网络地址,Java 也对 URL 进行了封装,java.net.URL 类常用方法如表:
方法 | 描述 |
---|---|
public URL(String protocol,String host,int port,String file) throws MalformedURLException | 根据协议、ip地址、端口号、资源名称获取 URL 对象 |
public final InputStream openStream() throws java.io.IOException | 获取输入流对象 |
在本机(127.0.0.1)上部署服务(web_need),使用 URL 读取该服务的 login.jsp 资源,代码如下:
public class TestURL {
public static void main(String[] args) {
InputStream inputStream = null;
Reader reader = null;
BufferedReader bufferedReader = null;
try {
URL url = new URL("http","127.0.0.1",8080,"/web_need/login.jsp");
inputStream = url.openStream();
reader = new InputStreamReader(inputStream);
bufferedReader = new BufferedReader(reader);
String str = null;
while((str = bufferedReader.readLine()) != null) {
System.out.println(str);
}
} catch (Exception e) {
// TODO: handle exception
} finally {
try {
bufferedReader.close();
reader.close();
inputStream.close();
} catch (IOException e2) {
// TODO: handle exception
}
}
}
}
运行结果:
同时也可以通过 URL 获取资源的其他信息,如主机地址、端口等,具体操作如下:
public class TestURL {
public static void main(String[] args) {
try {
URL url = new URL("http","127.0.0.1",8080,"/web_need/login.jsp");
System.out.println(url.getHost());
System.out.println(url.getPort());
System.out.println(url.getPath());
} catch (Exception e) {
// TODO: handle exception
}
}
}
运行结果:
127.0.0.1
8080
/web_need/login.jsp
URLConnection
URLConnection 用来描述 URL 指定资源的连接,是一个抽象类,常用的子类有 HttpURLConnection,URLConnection 底层是通过 HTTP 协议来处理的,它定义了访问远程网络资源的方法。通过 URLConnection 可以获取到 URL 资源的相关信息,该类常用的方法如表:
方法 | 描述 |
---|---|
public int getContentLength() | 返回资源的长度,返回值为 int 类型 |
public long getContentLengthLong() | 返回资源的长度,返回值为 long 类型 |
public String getContentType() | 返回资源的类型 |
public abstract void connect() throws IOException | 判断连接的打开或关闭状态 |
public Inputstream getInputStream() throws IOException | 获取输入流对象 |
接下来我们学习 URLConnection 如何使用,例如要获取上节 URL 资源的相关信息和具体内容,就可以通过 URLConnection 来完成
public class TestURLConnection {
public static void main(String[] args) {
InputStream inputStream = null;
Reader reader = null;
BufferedReader bufferedReader = null;
try{
URL url = new URL("http://localhost:8080/web_need/login.jsp");
URLConnection urlConnection = rul.openConnection();
System.out.println(urlConnection.getURL());
System.out.println(urlConnection.getContentLengthLong());
System.out.println(urlConnection.getContentLength());
System.out.println(urlConnection.getContentType());
inputStream = urlConnection.getInputStream();
reader = new InputStreamReader(inputStream);
bufferedReader = new BufferedReader(reader);
String str = null;
while((str = bufferedReader.readLine()) != null){
System.out.println(str);
}
} catch (Exception e) {
// TODO: handle exception
} finally {
try {
bufferedReader.close();
reader.close();
inputStream.close();
} catch (IOException e2) {
// TODO: handle exception
}
}
}
}
运行结果:
TCP 协议
TCP 是面向连接的运输层协议,比较复杂,应用程序在使用 TCP 协议前必须先建立连接,才能传输数据,数据传输完毕之后需要释放已经建立的连接。TCP 的有点是非常可靠,通过 TCP 传输数据,不会出现丢失的情况,并且数据是暗号先后顺序依次到达的。缺点是速度慢、效率低,实际开发中需要根据具体的业务需求来选择,对安全性要求较高的系统需要使用 TCP 协议 (例如金融系统),必须先确保用户成功登录,才能进行后续的操作。
Java 通过 Socket 来完成 TCP 程序的开发, Socket 是一个类,使用该类可以在服务端与客户端之间建立可靠的连接。在实际开发中,Socket 表示客户端,服务端使用 ServerSocket 来表示,ServerSocket 也是一个类,ServerSocket 和 Socket 都存放在 java.net 包中。具体的开发思路是在服务端创建 ServerSocket 对象,然后通过该对象的 accept() 方法可以接受到若干个表示客户端的 Socket 对象。
ServerSocket 类的常用方法如表:
方法 | 描述 |
---|---|
public ServerSocket(int port) throws IOException | 根据端口创建 ServerSocket 实例对象 |
public ServerSocket(int port,int backlog) throws IOException | 根据端口和 backlog 创建 ServerSocket 实例对象 |
public ServerSocket(int port,int backlog,InetAddress address) throws IOException | 根据端口、backlog 和 IP 地址创建 ServerSocket 对象 |
public ServerSocket() throws IOException | 创建没有绑定服务器的 ServerSocket 实例对象 |
public synchronized int getSoTimeout() throws IOException | 获取 Sotimeout 的设置 |
public InetAddress getInetAddress() | 获取服务器的 IP 地址 |
public Socket accept() throws IOException | 等待客户端请求,并返回 Socket 对象 |
public void close() throws IOException | 关闭 ServerSocket |
public boolean isClosed() | 返回 ServerSocket 的关闭状态 |
public void bind(SocketAddress endpoint) throws IOException | 将 ServerSocket 实例对象绑定到指定地址 |
public int getLocalPort() | 返回 ServerSocket 的端口 |
Socket 类的常用方法如表:
方法 | 描述 |
---|---|
public Socket(String host, int port) throws UnknownHostException,IOException | 根据主机、端口创建要连接的 Socket 对象 |
public Scoket(InetAddress host,int port) throws IOException | 根据 IP 地址、端口创建要连接的 Socket 对象 |
public Socket(String host,int port,InetAddress localAddress,int localPort) throws IOException | 根据主句、端口创建要连接的 Socket 对象并将其连接到指定远程主机上的指定端口 |
public Socket(InetAddress host,int port,InetAddress localAddress,int localPort) throws IOException | 根据主机、端口创建要连接的 Socket 对象并将其连接到指定远程地址上的指定端口 |
public Socket() | 创建没有连接的 Socket 对象 |
public InputStream getInputStream() throws IOException | 返回 Socket 的输入流 |
public synchronized void close() throws IOException | 关闭 Socket |
public boolean isClose() | 返回 Sockeet 的关闭状态 |
下面来看 ServerSocket 和 Socket 的实际应用,首先启动 ServerSocket,等待接收客户端请求。当接受盗客户端请求后,打印“接收到了客户端请求” 信息,同时向客户端返回 “Hello World”。
服务端代码:
public class ServerSocketDemo {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream outputStream = null;
InputStream inputStream = null;
DataInputStream dataInputStream = null;
DataOutputStream dataOutputStream = null;
try {
serverSocket = new ServerSocket(8080);
System.out.println("------服务端-------");
System.out.println("已启动,等待接收客户端请求...");
while(true) {
socket = serverSocket.accept();
inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
String request = dataInputStream.readUTF();
System.out.println("接收到客户端请求:"+request);
outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
String response = "Hello World";
dataOutputStream.writeUTF(response);
System.out.println("给客户端做出响应:"+response);
}
} catch (IOException e) {
// TODO: handle exception
} finally {
try {
dataOutputStream.close();
outputStream.close();
dataInputStream.close();
inputStream.close();
socket.close();
serverSocket.close();
} catch (IOException e2) {
// TODO: handle exception
}
System.out.println("服务端已结束服务!");
}
}
}
运行结果:
------服务端-------
已启动,等待接收客户端请求…
此时程序并没有结束,在等待客户端的连接请求。我们写好客户端代码并运行:
public class SocketDemo {
public static void main(String[] args) {
Socket socket = null;
OutputStream outputStream = null;
InputStream inputStream = null;
DataOutputStream dataOutputStream = null;
DataInputStream dataInputStream = null;
try {
socket = new Socket("127.0.0.1",8080);
System.out.println("-----客户端-----");
String request = "你好!";
System.out.println("客户端说:"+request);
outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeUTF(request);
inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
String response = dataInputStream.readUTF();
System.out.println("服务端响应:"+response);
} catch (IOException e) {
// TODO: handle exception
} finally {
try {
dataOutputStream.close();
outputStream.close();
dataInputStream.close();
inputStream.close();
socket.close();
} catch (IOException e2) {
// TODO: handle exception
}
}
}
}
运行结果:
------服务端-------
已启动,等待接收客户端请求…
接收到客户端请求:你好!
给客户端做出响应:Hello World
-----客户端-----
客户端说:你好!
服务端响应:Hello World
UDP 协议
TCP 协议可以建立稳定可靠的连接,保证数据的完整性。但是 TCP协议的确定也很明显,先建立可靠连接,再进行操作的方法必然会造成系统运行效率低下。在实际开发中某些业务场景对系统的运行效率要求较高,使用 TCP 协议很显然就不合适了,这时候就需要使用另一种传输协议——UDP。
UDP 所有的连接都是不可靠的,既不需要建立连接,直接发送数据即可。所以 UDP 的速度更快,但是可能会造成数据丢失,安全性不高,追求速度的应用可以选择 UDP。例如语音聊天或者视频聊天,对弈这类应用流畅性更重要,偶尔丢失几个数据包并不会有太大影响。 Java 提供了 DatagramSocket 类和 DatagramPacket 类,来帮助开发者编写基于 UDP 协议的程序,DatagramSocket 类的常用方法如表:
方法 | 描述 |
---|---|
public DatagramSocket(int port) throws SocketException | 根据端口创建 DatagramSocket 实例对象 |
public void send(DatagramPacket p) throws IOException | 发送数据报 |
public synchronized void receive(DatagramPacket p) throws IOException | 接收数据报 |
public InetAddress getInetAddress() | 获取 DatagramSocket 对应的 InetAddress 对象 |
public boolean isConnected | 判断是否连接到服务 |
DatagramPacket 类的常用方法如表:
方法 | 描述 |
---|---|
public DatagramPacket(byte[] buf[],int length,InetAddress address,int port) | 根据发送的数据、数据长度、IP地址、端口,创建 DatagramPacket 实例对象 |
public synchronized byte[] getData() | 获取接收的数据 |
public synchronized int getLength() | 获取数据长度 |
public synchronized int getPort() | 获取发送数据的 Socket 端口 |
public synchronized SocketAddress getSocketAddress() | 获取发送数据的 Socket 信息 |
两个终端通过 UDP 协议进行通信的具体实现如下:
public class TerminalA {
public static void main(String[] args) throws Exception {
//接收数据
byte[] buff = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(buff,buff.length);
DatagramSocket datagramSocket = new DatagramSocket(8181);
datagramSocket.receive(datagramPacket);
String mess = new String(datagramPacket.getData(),0,datagramPacket.getLength());
System.out.println("我是 TerminalA ,接收到了"+datagramPacket.getPort()+"传来的数据:"+mess);
//发送数据
String reply = "我是 TerminalA ,已接收到你发来的数据";
SocketAddress socketAddress = datagramPacket.getSocketAddress();
DatagramPacket datagramPacket2 = new DatagramPacket(reply.getBytes(),reply.getBytes().length,socketAddress);
datagramSocket.send(datagramPacket2);
}
}
public class TerminalB {
public static void main(String[] args) throws Exception {
String mess = "我是 TerminalB,你好!";
//发送数据
InetAddress inetAddress = InetAddress.getByName("localhost");
DatagramPacket datagramPacket = new DatagramPacket(mess.getBytes(),mess.getBytes().length,
inetAddress,8181);
DatagramSocket datagramSocket = new DatagramSocket(8080);
datagramSocket.send(datagramPacket);
//接收数据
byte[] buff = new byte[1024];
DatagramPacket datagramPacket2 = new DatagramPacket(buff,buff.length);
datagramSocket.receive(datagramPacket2);
String reply = new String(datagramPacket2.getData(),0,datagramPacket2.getLength());
System.out.println("我是 TerminalB,接收到了"+datagramPacket2.getPort()+"返回的数据:"+reply);
}
}
运行结果:
我是 TerminalA ,接收到了8080传来的数据:我是 TerminalB,你好!
我是 TerminalB,接收到了8181返回的数据:我是 TerminalA ,已接收到你发来的数据
多线程下的网络编程
前面章节的代码都是基于单点连接的方式,即一个服务端对应一个客户端。实际运行环境中是一个服务端需要对应多个客户端的,这种情况我们可以使用多线程来模拟。
public class ServerThread {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(8080);
System.out.println("----服务器已启动-----");
while(true) {
Socket socket = serverSocket.accept();
new Thread(new ServerRunnable(socket)).start();
}
} catch (IOException e) {
// TODO: handle exception
}
}
}
class ServerRunnable implements Runnable{
private Socket socket;
public ServerRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream inputStream = null;
DataInputStream dataInputStream = null;
try {
inputStream = this.socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
String message = dataInputStream.readUTF();
System.out.println(message);
} catch (IOException e) {
// TODO: handle exception
} finally {
try {
dataInputStream.close();
inputStream.close();
socket.close();
} catch (IOException e2) {
// TODO: handle exception
}
}
}
}
public class ClientThread {
public static void main(String[] args) {
for(int i = 0; i < 20; i++) {
new Thread(new ClientRunnable(i)).start();
}
}
}
class ClientRunnable implements Runnable{
private int num;
public ClientRunnable(int num) {
this.num = num;
}
@Override
public void run() {
Socket socket = null;
OutputStream outputStream = null;
DataOutputStream dataOutputStream = null;
try {
socket = new Socket("localhost",8080);
String mess = "我是客户端"+this.num;
outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeUTF(mess);
} catch (IOException e) {
// TODO: handle exception
e.printStackTrace();
} finally {
try {
dataOutputStream.close();
outputStream.close();
socket.close();
} catch (IOException e2) {
// TODO: handle exception
}
}
}
}
运行结果:
----服务器已启动-----
我是客户端3
我是客户端15
我是客户端11
我是客户端12
我是客户端16
我是客户端7
综合练习
使用 Socket 和 多线程编写一个简单的聊天小程序,要求客户端和服务端交替发送消息,在客户端和服务端都能看见到彼此的聊天记录。
服务端代码:
public class Server {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
try {
serverSocket = new ServerSocket(8080);
System.out.println("服务器已启动...");
while(true) {
socket = serverSocket.accept();
new Thread(new SocketThread(socket)).start();
}
} catch (IOException e) {
// TODO: handle exception
}
}
}
class SocketThread implements Runnable{
private Socket socket;
public SocketThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream inputStream = null;
DataInputStream dataInputStream = null;
OutputStream outputStream = null;
DataOutputStream dataOutputStream = null;
String message = null;
Scanner scanner = new Scanner(System.in);
try {
while(true) {
//读
inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
message = dataInputStream.readUTF();
System.out.println("客户端:"+message);
//写
outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
System.out.print("服务器:");
message = scanner.nextLine();
dataOutputStream.writeUTF(message);
}
} catch (IOException e) {
// TODO: handle exception
}
}
}
客户端:
public class Client {
public static void main(String[] args) {
Socket socket = null;
InputStream inputStream = null;
DataInputStream dataInputStream = null;
OutputStream outputStream = null;
DataOutputStream dataOutputStream = null;
String message = null;
Scanner scanner = new Scanner(System.in);
try {
System.out.println("客户端已经启动...");
socket = new Socket("127.0.0.1",8080);
while(true) {
//写
outputStream = socket.getOutputStream();
dataOutputStream = new DataOutputStream(outputStream);
System.out.print("客户端:");
message = scanner.nextLine();
dataOutputStream.writeUTF(message);
//读
inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
message = dataInputStream.readUTF();
System.out.println("服务端:"+message);
}
} catch (IOException e) {
// TODO: handle exception
}
}
}
运行结果:
客户端已经启动…
客户端:问你个问题
服务端:嗯嗯,你讲
客户端:土豆可以做成土豆泥,红豆可以做成红豆泥,你觉得你可以做成什么?
服务端:不知道呐
客户端:笨蛋,是我爱泥