目录
为什么需要网络编程
获取更加丰富的网络资源.
用户在浏览器中,打开在线视频网站,观看视频.这一过程,实质上是通过网络,获取到网络上的一个视频资源.与本地打开视频文件类似,只不过视频文件这个资源是来自于网络.
相对于本地资源来说,网络提供了更为丰富的网络资源.
网络资源,就是在网络中可以获取的各种数据资源.而所有的数据资源,都是通过网络编程来进行数据传输的.
什么是网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输).
当然,我们只要满足进程不同即可,所以即便是同一个主机,只要是不同的进程,基于网络来传输数据,也是属于网络编程的.
Socket套接字
概念:Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基 于Socket套接字的网络程序开发就是网络编程.
Socket 被译为套接字.
网络编程的核心就是Socket API,这是操作系统给应用程序提供的网络编程API.Socket API是站在传输层的角度和应用层直接进行交互.
可以认为socket api 是和传输层密切相关的.
传输层里,提供了两个最核心的协议,UDP 和 TCP,因此socket api 也提供了;两种风格 UDP 和TCP.
简单认识UDP 和 TCP协议
UDP 无连接 不可靠传输 面向数据报 全双工
TCP 有连接 可靠传输 面向字节流 全双工
连接:打电话就是有连接的,通信双方需要建立了连接才能进行通信,连接的建立需要对方来"接受",如果连接没建立好,就无法进行通信;发短信/发微信 就是无连接的,直接发送即可.
可靠:网络环境天然是复杂的,不可能保证传输的数据100%到达.可靠传输,就是发送方能知道自己的消息是发送过去了,还是在传输过程中丢了.
需要注意的是,可靠不可靠和有没有连接是没有任何关系的,比如带有已读功能的app如钉钉,就是无连接 可靠传输.
面向字节流:数据传输和文件读写类似,是"流式"的.
面向数据报:数据传输是以一个个的"数据报"为基本单位.(一个数据报可能是若干个字节,是带有一定的格式的)
全双工:一个通信通道,可以双向传输,即可以发送,也可以接收.(半双工,是单向传输的,类似于家里的水管,只能往外流水,不能往里灌水)
为什么UDP 和TCP都是全双工的?
网线看起来像是水管,所以容易误认为是半双工的,其实不是,一根网线里其实有八根线.类似于八车道的公路,一半是往一个方向的,一半是往反方向的.
基于UDP编写一个简单的客户端服务器网络通信程序
Java标准库里基于UDP socket提供了两个最核心的类DatagramSocket和DatagramPacket,通过这两个类就可以实现基于UDP的网络编程.
DatagramSocket
方法:
receive方法相当于传入一个空的对象,receive方法内部,会对参数的这个空对象进行内容填充,从而构造出数据,此处的参数也是一个"输出型参数".
DatagramPacket
使用SocketAddress类表示IP+port
方法
UDP版本回显服务器客户端代码
编写一个最简单的UDP版本的客户端服务器程序:回显服务器(ehco server).
一个普通的服务器:收到请求,根据请求计算相应,返回响应.
上述是一个普通服务器的工作过程,而echo server省略了其中的"计算请求计算相应",请求是什么,就返回什么.(这个代码没有实际的业务,也没有什么太大的作用和意义,只是为了展示socket api的基本用法)
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//UDP版本的回显服务器
public class UdpEchoServer {
//网络编程,本质上是要操作网卡.
//但是网卡不方便直接操作,在操作系统内核中,使用了一种特殊的叫做"socket"这样的文件来抽象表示网卡
//因此进行网络通信,势必需要现有一个socket对象
private DatagramSocket socket = null;
//对于服务器来说,创建 socket 对象的同时,要让它绑定上一个具体的端口号.
//服务器一定要关联上一个具体的端口号!
//服务器在网络传输中,是被动的一方,如果是操作系统随机分配的端口号,此时客户端就不知道这个端口是什么了,也就无法进行通信了!!
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器不是只给一个客户端提供服务,而是服务很多客户端
while (true){
//只要客户端过来,就可以提供服务
//1.读取客户端发来的请求是什么
//receive 方法的参数是一个输出型参数,需要先构造好一个空白的 DatagramPacket 对象,交给receive来进行填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//此时这个 DatagramPacket 是一个特殊的对象,并不方便直接进行处理,可以把这里包含的数据拿出来,构造成一个字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应,由于此处是回显服务器,响应和请求相同.
String response = process(request);
//3.把相应写回到客户端. send 方法的参数也是 DatagramPacket.需要把这个Packet 对象构造好.
DatagramPacket responsePacket = new DatagramPacket(request.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印当前这次请求响应的处理中间结果
System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),
request,response);
}
}
///这个方法就表示"根据请求计算相应"
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
//端口号的指定,可以随便指定
//1024-65535 这个范围里随便挑个数字
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//UDP版本的回显客户端
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp = null;
private int serverPort= 0;
//一次通信,需要两个ip,两个端口.
//客户端的ip 是127.0.0.1
//客户端的port 是系统自动分配的.
//服务器的ip和端口也要告诉客户端,才能顺利的把消息发给服务器.
public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
//构造这个socket 对象,不需要显式绑定一个端口(让操作系统自动分配一个端口,随机挑一个空闲的)
//端口号用来标识//区分一个进程
//因此不允许一个端口同时被多个进程使用(前提是同一个主机)
//客户端如果显式指定窗口,可能就和客户端电脑上的其他程序的端口冲突了.这一冲突就可能导致程序无法正常通信了.
//那么为什么服务器指定端口不怕重复呢?
//服务器是程序员自己手里的机器,上面运行什么,都是程序员可控的,程序员就可以安排哪个程序用哪个端口,这是可控的.
//客户端的机器在用户手里,不同用户手里的机器,千奇百怪,上面运行着什么样的程序,也各有不同,这是不可控的.
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true){
//1.从控制台读取要发送的数据
System.out.print("> ");
String request = scanner.next();
if (request.equals("exit")){
System.out.println("goodbye!");
break;
}
//2.构造成UDP请求,并发送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
//3.读取服务器的UDP响应,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
//4.把解析好的结果显示出来.
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
端口冲突
端口冲突会报这样的异常:
基于TCP编写一个简单的客户端服务器网络通信程序
TCP提供的API主要是两个类
ServerSocket和Socket
ServerSocket
专门给服务器使用的Socket对象.
Socket
是既会给客户端使用,也会给服务器使用.
需要注意的是:TCP不需要一个类来表示"TCP数据报",TCP不是以数据报为单位进行传输的,是以字节的方式,流式传输.
ServerSocket
构造方法:
方法
Socket
Socket在服务器这边是由accept返回的;在客户端这边,是我们代码里构造的.构造的时候指定一个IP和端口号.(此处指定的IP和端口号是服务器的IP和端口),有了这个信息,就能和这个服务器建立连接了.
TCP版本的回显服务器客户端代码
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 {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器!");
while (true){
//使用这个 clientSocket 和具体的客户端进行交流
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
//使用这个方法来处理一个连接
//这一个连接对应到一个客户端,但是这里可能会涉及到多次交互
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//基于上述Socket对象和客户端进行通信
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
//由于要处理多个请求和响应,也是使用循环来进行
while (true){
//1.读取请求
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()){
//没有下个数据,说明读完了.(客户端关闭了连接)
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//此处使用next 是一直读取到换行符/空格//其他空白符结束,但是最终返回的结果里不包含上述空白符
String request = scanner.next();
//2.根据请求,构造响应
String response = process(request);
//3.返回响应结果
// OutputStream 没有write String 这样的功能,可以把String里的字节数组拿出来,进行写入
PrintWriter printWriter = new PrintWriter(outputStream);
//此处使用 println 来写入,让结果中带有一个换行,方便对端来接受解析;
printWriter.println(response);
//flush用来刷新缓冲区,保证当前写入的数据确实是发送出去了
printWriter.flush();
System.out.printf("[%s:%d] req: %s;resp: %s \n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
request,response);
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
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 = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// Socket 构造方法, 能够识别 点分十进制格式的 IP 地址. 比 DatagramPacket 更方便.
// new 这个对象的同时, 就会进行 TCP 连接操作.
socket = new Socket(serverIp, serverPort);
}
public void start() {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 1. 先从键盘上读取用户输入的内容
System.out.print("> ");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("goodbye");
break;
}
// 2. 把读到的内容构造成请求, 发送给服务器.
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
// 此处加上 flush 保证数据确实发送出去了.
printWriter.flush();
// 3. 读取服务器的响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4. 把响应内容显示到界面上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
代码优化
当前代码中,还有一个非常重要的问题!我们当前的服务器,同一时刻只能处理一个连接(只能给一个客户端提供服务),这是不合理的.
当客户端连接上服务器后,代码就执行到了processConnection这个方法里的while循环中了,此时就意味着,只要循环不结束,processConnection方法就结束不了,进一步的也就无法第二次调用到accept.
那我们基于上述问题,就可以使用多线程的方法来解决.
主线程,专门负责进行accept,每次收到一个连接,创建新线程,由这个新的线程负责处理这个新的客户端.
每个线程是独立的执行流,每个独立的执行流,是各自执行各自的逻辑,彼此之间是并发的关系.
这样就把processConnection方法的执行从主线程中拿出来了,交给新的线程去处理.
如果我们的服务器,客户端很多,有很多客户端频繁的建立连接,就需要频繁的创建/销毁线程,针对此问题,我们可以使用线程池来解决.