一、网络编程基础
1.1 为什么需要网络编程
丰富的网络资源,用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。 与本地打开视频文件类似,只是视频文件这个资源的来源是网络。 相比本地资源来说,网络提供了更为丰富的网络资源: 所谓的网络资源,其实就是在网络中可以获取的各种数据资源。 而所有的网络资源,都是通过网络编程来进行数据传输的
1.2 什么是网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。 当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。 特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。 但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:
- 进程A:编程来获取网络资源
- 进程B:编程来提供网络资源
1.3 网络编程中的基本概念
(1)发送端和接收端
在一次网络数据传输时: 发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。 接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。 收发端:发送端和接收端两端,也简称为收发端。 注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
(2)请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输: 第一次:请求数据的发送 第二次:响应数据的发送
(3)客户端和服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。 客户端:获取服务的一方进程,称为客户端。
对于服务来说,一般是提供:
- 客户端获取服务资源
- 客户端保存资源在服务端
(4)常见的客户端服务器模型 最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
- 客户端先发送请求到服务端
- 服务端根据请求数据,执行相应的业务处理
- 服务端返回响应:发送业务处理结果
- 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)
(5) 客户端和服务器之间的交互方式
- 一问一答
客户端给服务器发送一个请求,服务器给客户端返回一个响应 - 多问一答
客户端发送多个请求,服务器返回一个响应,比如上传文件(文件过大时,需要拆分成个请求,多次上传) - 一问多答
客户端发一个请求,服务器返回多个响应 ,比如下载文件(文件过大时,文件被拆分成多响应后返回) - 多问多答
客户端发送多个请求,服务端返回多个响应,比如远程控制 网络编程:通过写代码,实现两个/多个进程之间,通过网络,进行相互通信 但之前我们学习进程时了解到进程具有隔离性,要想实现进程间通信,需要借助一个每个进程都能访问到的公共区域,完成数据交换 网络编程其实也是进程间通信的一种方式,此处借助的公共区域就是网卡
(6)进行网络编程需要使用操作系统提供的API 传输层提供了网络通信的API,这些API也叫做socket API,操作系统提供的原生API,是C语言的,因此JVM就把C风格的socket api 封装了一下,变成了Java中面向对象风格的api
二、Socket套接字
2.1 概念
Socket套接字,是系统提供于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程
2.2 分类
Socket套接字主要针对传输层协议划分为如下三类: (1)流套接字:使用传输层TCP协议 TCP的特点:
- 有连接(类似于打电话,先建立连接,再进行通话)
- 可靠传输(发送的数据对方收没收到,发送方有感知)
- 面向字节流
- 有接收和发送缓冲区
- 大小不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。 (2)数据报套接字:使用传输层UDP协议 UDP的特点
- 无连接(类似于发微信,不必建立连接,可直接发送消息)
- 不可靠传输(发送的数据对方收没收到,发送方并不关心)
- 面向数据报
- 有接受缓冲区,无发送缓冲区
- 大小受限:依次最多传输64k
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每接接收1个字节。 (3)原始套接字 原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据
2.3 Java数据报套接字通信模型
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数 据报,一次接收全部的数据报。 java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用DatagramPacket 作为发送或接收的UDP数据报。对于一次发送及接收UDP数据报的流程如下:
以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请 求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:
2.4 Java流套接字通信模型
【Socket编程注意事项】
- 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景,一般是不同的主机
- 目的IP和目的端口号,标识了一次数据传输时要发送的数据的终点主机和进程
- Socket编程我们是使用流套接字和数据包套接字,基于传输层的TCP或UDP协议,但应用层协议,也需要考虑
- 端口占用问题,如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。对于java进程来说,端口被占用的常见报错信息如下v:
此时需要检查进程B绑定的是哪个端口,再查看该端口被哪个进程占用。以下为通过端口号查进程的方式:
- 在cmd输入 netstat -ano | findstr 端口号 ,则可以显示对应进程的pid。如以下命令显示了70进程的pid
- 再任务管理器中,通过进程PID找到对应的进程
- 如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B
如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口。
2.5 UDP数据包套接字编程
DatagramSocket API
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报 DatagramSocket构造方法: DatagramSocket 方法: 【补充】:
- DatagramSocket 属于是socket类,本质上相当于是一个"文件",在系统有一个种特殊的socket文件,这种文件对应到网卡设备,此时构造一个DatagramSocket对象, 就相当于是打开了一个内核中的socket文件
- 打开socket文件之后,就能进行传输数据了
DatagramPacket API
DatagramPacket 是UDP Socket发送和接收的数据报 DatagramPacket 的构造方法: DatagramPacket 方法: 构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。 InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法: 【关于端口号】:
在操作系统中端口号的范围是0-65535,程序如果想要进行网络通信,就需要获取到一个端口号,端口号相当于用来在网络上区分进程的身份标识符(操作系统收到网卡的数据,就可以根据网络数据报中的端口号来决定要将这个数据交给哪个进程) 分配端口号有两种方式:程序员手动指定和系统自动分配
【UDP版本的回显服务器-客户端】(echo server) 服务器代码:
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket socket=null;
public UdpEchoServer(int port) throws SocketException {
socket=new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动:");
while (true){
DatagramPacket requsetPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requsetPacket);
String request=new String(requsetPacket.getData(),0, requsetPacket.getLength());
String response=process(request);
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),0,response.getBytes().length
,requsetPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] res=%s;resp=%s\n",requsetPacket.getAddress().toString(),
requsetPacket.getPort(),request,response);
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer=new UdpEchoServer(8000);
udpEchoServer.start();
}
}
客户端代码:
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket=null;
public UdpEchoClient() throws SocketException {
socket= new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
while (true){
System.out.println(">");
String request=scanner.next();
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName("127.0.0.1"),8000);
socket.send(requestPacket);
DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response=new String(responsePacket.getData(),0, responsePacket.getLength());
System.out.printf("req:%s;resp:%s\n",request,response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client=new UdpEchoClient();
client.start();
}
}
【执行结果】
【同时启动多个客户端的设置过程】
【服务器端口号和客户端端口号】
服务器,端口号一般都是手动指定的,如果由操作系统自动分配,客户端就不知道服务器的端口号是多少,也就无法向客户端发送请求了 客户端,端口一般是自动分配的,因为客户端程序是安装在用户电脑上的,用户电脑当前运行哪些程序,是不可控的,如果要是手动指定端口,就可能发生端口冲突
【翻译服务器】(英译汉)
服务器代码:
package network;
import java.io.IOException;
import java.net.SocketException;
import java.security.PrivateKey;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer {
private HashMap<String,String> dict=new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("fuck","卧槽");
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"这个俺也不会");
}
public static void main(String[] args) throws IOException {
UdpDictServer server=new UdpDictServer(8000);
server.start();
}
}
客户端代码同上
2.6 TCP流套接字编程
ServerSocket API
ServerSocket是创建TCP服务端的Socket的API ServerSocket构造方法 ServerSocket 方法: Socket API Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法 Socket方法 【说明】:
- accept 方法没有参数,返回值是Socket对象,功能是等待有客户端和服务器建立连接,accept会把这个连接获取到进程中,进一步的通过返回值的Socket对象和客户端进行交互
- Socket:服务器和客户端都会使用Socket,通过Socket对象,就可以进行发送/接收数据了,这里的传输数据,不是直接通过Socket对象,而是Socket内部包含了输入流(接收)和输出流(发送)对象
【举例理解ServerSocket和Socket的作用】
对于售楼来说,是有明确的分工的,有人是负责在外场拉客的,有人负责为拉到的客人提供服务 ServerSocket起到的作用就相当于是外场拉客 Socket起到的作用就相当于在内场提供服务
【TCP中的长连接与短链接】
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接。 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。 区别:
- 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
- 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
- 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。 由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。 一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求。 实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。
【TCP版本的回显服务器-客户端】(echo server) 服务器代码:
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
ServerSocket serverSocket=null;
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动:");
while (true){
Socket clientSocket=serverSocket.accept();
processConnect(clientSocket);
}
}
private void processConnect(Socket clinetSocket) throws IOException {
System.out.printf("[%s %d] 建立连接\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
try(InputStream inputStream=clinetSocket.getInputStream();
OutputStream outputStream= clinetSocket.getOutputStream();){
Scanner scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
if(!scanner.hasNext()){
System.out.printf("[%s %d] 断开连接\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
break;
}
String request=scanner.next();
String response=process(request);
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s %d] req:%s;resp:%s\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort(),
request,response);
}
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server=new TcpEchoServer(8000);
server.start();
}
}
客户端代码:
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket=new Socket();
public TcpEchoClient() throws IOException {
socket=new Socket("127.0.0.1",8000);
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream= socket.getOutputStream()){
Scanner scanner1=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
System.out.println(">");
String request=scanner.next();
printWriter.println(request);
printWriter.flush();
String response=scanner1.next();
System.out.printf("req:%s;resp:%s\n",request,response);
}
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client=new TcpEchoClient();
client.start();
}
}
【注意细节】
- scanner.next的问题
- clientSocket使用完要关闭
ServerSocket的生命周期是跟随整个程序的,clientSocket的生命周期只是当前连接,在该连接使用完之后,应该及时关闭,如果不关闭,就会造成资源泄露,ServerSocket只有一个,clientSocket会有无数个,每个客户端与服务器好连接后都会有一个。 - 使服务器能同时处理多个客户端的请求
|