目录
为什么需要网络编程
为什么需要网络编程? —— 丰富的网络资源用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。 与本地打开视频文件类似,只是视频文件这个资源的来源是网络。 相比本地资源来说,网络提供了更为丰富的网络资源:所谓的网络资源,其实就是在网络中可以获取的各种数据资源。 而所有的网络资源,都是通过网络编程来进行数据传输的
什么是网络编程
什么是网络编程网络编程,指网络上的主机,通过 不同的进程 ,以编程的方式实现 网络通信(或称为网络数据传输) 。
- 当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据, 也属于网络编程。
- 特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。 但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:
- 进程A:编程来获取网络资源
- 进程B:编程来提供网络资源
- 一般意义上的网络通信,一般站在应用层视角去探讨,所以一般的意义上的网络编程,多是在传输层上进行讨论
传输层的两个重要协议
- UDP User Datagram Protocol 用户报文协议 UDP没有做任何处理,保存网络的原生态,是不可靠,无连接,面向数据报文的一种协议
- TCP Transmission Control Protocol 传输控制协议 可靠,有连接,面向字节流的一种协议
- 都是传输层协议,都需要实现进程到进程的通信,都是站在应用层的角度
网络层的协议是IP协议
如何建立一条网络上(逻辑上)的通信线路
发送端和接收端在一次网络数据传输时:
- 发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
- 接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
- 收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。请求和响应一般来说,获取一个网络资源,涉及到两次网络数据传输:
- 第一次:请求数据的发送
- 第二次:响应数据的发送。
客户端和服务端
- 服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
- 客户端:获取服务的一方进程,称为客户端。
对于服务来说
- 一般是提供:客户端获取服务资源
- 客户端保存资源在服务端
好比在银行办事:
- 银行提供存款服务:用户(客户端)保存资源(现金)在银行(服务端)
- 银行提供取款服务:用户(客户端)获取服务端资源(银行替用户保管的现金)
什么是套接字Socket
概念Socket 套接字,是由系统提供用于网络通信的技术,是基于 TCP/IP 协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程
分类
流套接字 :使用传输层 TCP 协议
- 有连接
- 可靠传输
- 面向字节流
- 有接收缓冲区,也有发送缓冲区
- 大小不限
对于字节流来说,可以简单的理解为,传输数据是基于 IO 流,流式数据的特征就是在 IO 流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。数据报套接字 :使用传输层 UDP 协议
- 无连接
- 不可靠传输
- 面向数据报
- 有接收缓冲区,无发送缓冲区
大小受限:一次最多传输 64k对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如 100 个字节,必须一次发送,接收也必须一次接收100 个字节,而不能分 100 次,每次接收 1 个字节Java数据报套接字通信模型对于一次发送及接收UDP数据报的流程如下:
提供多个客户端的请求处理及响应,流程如下
Java使用UDP协议
套接字的创建
DatagramSocket 构造方法
- UDP服务器(Server)采用一个固定端口,方便客服端进行通信,可能会有错误的风险,(该端口已经被其他进程占用)
- UDP客户端(Client),不需要采用固定端口(可以采用)
DatagramSocket 一些常用方法
- 一旦通信的双方逻辑意义上有了通信线路,双方地位是平等的(谁都可以作为接收方,谁都可以作为发送方)
- 通信结束后,都需要进进行资源回收
- 对于接收方法,如果没有接收到一个数据报,进程会一直阻塞
数据报的创建
- 数据报是通信过程的数据抽象,就理解成通信过程中发送/接收一个信封
- 作为接收方,只需要提供存放数据的位置
- 作为发送方,将需要发送的数据,和要发送给谁(远端ip+端口)
数据包类的方法
- 对于服务器使用,来获得客户端的ip和port
- 对于接收者:拿到信的内容,对方进程发送的应用层的数据
例子 基于UDP的翻译服务器和客户端
服务端
package com.lsc.net_demo.udp; import com.lsc.net_demo.util.Log; import java.io.IOException; import java.net.*; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; public class TranslateServer { private static final HashMap<String,String> map=new HashMap<>(); //公开的ip地址,就看进程在哪个ip上 //公开的port,需要在程序中指定 public static final int PORT=8888; public static void main(String[] args) throws IOException { Log.println("准备进行字典的初始化"); initMap(); Log.println("完成字典的初始化"); Log.println("准备创建UDPSocket,端口是"+PORT); DatagramSocket socket=new DatagramSocket(PORT); Log.println("UDP Socket创建成功"); //作为服务器,是被动的,循环的进行请求,响应周期的处理 //等待处理,处理并发送响应,直到永远 while (true){ //1接收请求 byte []buf=new byte[1024];//1024 代表我们最大接收数据大小 DatagramPacket receivePacket=new DatagramPacket(buf, buf.length); Log.println("准备好接收DatagramPacket,最大大小为"+buf.length+"字节"); Log.println("开始接收请求"); socket.receive(receivePacket);//这个方法会阻塞,程序执行到这就不动了,除非有客户发送请求,才能继续 //2一旦走到此处,说明接收到了请求,拆信 //拆出对方的IP地址 InetAddress address=receivePacket.getAddress(); Log.println("对方的IP地址"+address); //拆出对方的端口 int port=receivePacket.getPort(); Log.println("对方的端口"+port); //拆出对方ip+端口 SocketAddress socketAddress=receivePacket.getSocketAddress(); Log.println("对象的完整地址"+socketAddress); //拆出对方发送的数据,其实这个data就是刚才定义的buf数组 byte[]data =receivePacket.getData(); Log.println("刚接收的对象的数据"+ Arrays.toString(data)); //拆出接收到的数据的大小 int length=receivePacket.getLength(); Log.println("接收的数据大小(字节)"+length); //3解析请求,意味着我们需要定义字节的应用层协议 //首先将字符集解码 String request=new String(data,0,length,"UTF-8"); String engWorld=request; Log.println("请求(英文单词)"+engWorld); //执行业务,进行翻译 String chiWorld=translate(engWorld); //按照应用层协议,封装响应 String response=chiWorld; //进行字符集编码 String->byte[] byte []sendBuf=response.getBytes("UTF-8"); //发送响应 //作为发送方需要提供数据报的类 DatagramPacket sentPacket=new DatagramPacket(sendBuf,0,sendBuf.length,//要发送的数据 socketAddress//从请求中拆出的对象地址 ); Log.println("准备好发送DatagramPacket并发送"); socket.send(sentPacket); Log.println("发送成功"); //本次请求-回应周期完成,继续下一次请求 } // socket.close(); } private static String translate(String engWorld) { String chiWorld=map.getOrDefault(engWorld,"查无此单词"); return chiWorld; } private static void initMap() { map.put("apple","苹果"); map.put("pear","梨子"); map.put("orange","橙子"); } }
客户端
package com.lsc.net_demo.udp; import com.lsc.net_demo.util.Log; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.util.Scanner; public class UserInputLoopClient { public static void main(String[] args) throws IOException { Scanner sc=new Scanner(System.in); //1创建UDPSocket Log.println("准备创建UDP socket"); DatagramSocket socket=new DatagramSocket(); Log.println("UDP socket创建结束"); System.out.println("请输入英文单词:"); while (sc.hasNextLine()) { //2发送请求 String engWorld = sc.nextLine(); Log.println("英文单词是" + engWorld); String request = engWorld; byte[] bytes = request.getBytes("UTF-8"); //手段构造服务器的地址 //现在我们的客服端和服务端都在一台主机,所以使用127.0.0.1 InetAddress loopbackAddress = InetAddress.getLoopbackAddress(); //端口使用Translate.PORT(8888) DatagramPacket sentPacket = new DatagramPacket(bytes, 0, bytes.length, loopbackAddress,//对方ip TranslateServer.PORT//对方的端口 ); Log.println("准备发送请求"); socket.send(sentPacket); Log.println("请求发送结束"); //3接收响应 byte[] buf = new byte[1024]; DatagramPacket receivePacket = new DatagramPacket(buf, buf.length); Log.println("准备接收响应"); socket.receive(receivePacket); byte[] data = receivePacket.getData(); int len = receivePacket.getLength(); String response = new String(data, 0, len, "UTF-8"); String chiWord = response; Log.println("翻译结果" + chiWord); System.out.println("请输入英文单词:"); } socket.close(); } }
Java使用TCP协议
服务端的Socket建立
- 有连接(先拨号,拨通才能通信) 无连接 接发信(无脑发送,对方在不在都没关系)
- 因为TCP是有连接的,服务器使用TCP Socket(传入的端口就是要公开的端口,一般称为监听端口)
服务端ServerSocket的方法
- 服务器的Socket(客户端对象)是通过accept中获取来的,所以客户端Socket对象需要主机手动实例
- 挂电话,谁都可以挂
客户端的Socket建立
- 一旦拿到socket对象拿到,双方是同时拿到的(电话是同时解通的),双方的地位就平等了,只需要分发送方和/接收方
客户端的Socket方法
- 拿到对方的IP地址/getRemote()拿到对方的端口
- 输入流:站在进程角度,是输入流(背后对象就是网卡,网卡抽象出来的TCP连接),所以是给接收方使用的
- 输出流:所以是给发送方方使用的
输入和输出流的使用
因为是基于字节流的,所以可以使用输入输出流
- 网络编程的输入输出流是站在进程的角度,所以输入是进程输入给别的对象,所以是给接收方使用,输出是别的东西输出给进程,所以是发送方使用
- 对于TCP来说,是基于字节流的,所以我们可以对输入流进行封装,封装成Scanner使用,对于输出流,先封装成OutputStream然后在封装成PrintWriter进行使用
- 对于输出的话,因为是要输出东西给进程, 所以需要刷新缓冲区,flush
TCP 中的长短连接TCP 发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
- 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数 据。
- 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以 多次收发数据
例子
- 短连接,就是我每次说一句话,就挂断电话,如果想继续发信息,需要重新打电话
- 长连接,就是打通电话,我把我想说的全部都说了,一句接着一句(我说一句,回应一句)
关于同时在线的服务器和客户端
- 短连接客户端和短连接服务器 支持同时在线,因为短连接是说一句就挂了,和服务器的连接也断了(其他客户端可以申请服务),所以可以支持多个连接
- 短链接客户端和长连接服务器,虽然是长连接服务器,但是是短连接客户端,客户端要求一次服务,就断开连接了(相当于我可以一直说话,但是我就是愿意说一句就挂断),跟前面的一种情况一致
- 长连接客户端和长连接服务器,就构成一个电话我可能会一直打,所以服务器一直被占用,其他长连接的 客户端就不能接收服务,不能支持同时在线,怎么解决呢?通过多线程解决,将提供服务的功能让子线程完成,主线程就只管服务端和客户端之间关系建立
例子 基于TCP翻译服务器和客户端(短链接)
服务器
package com.lsc.net_demo.tcp; import com.lsc.net_demo.util.Log; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.*; import java.util.Arrays; import java.util.HashMap; import java.util.Scanner; /** * 短连接就算每次发送一个信息,就会挂掉电话 */ public class TranslateServerShortConnection { public static final int PORT = 8888; public static void main(String[] args) throws Exception { initMap(); ServerSocket serverSocket = new ServerSocket(PORT); while (true) { // 接电话 Log.println("等待对方来连接"); Socket socket = serverSocket.accept();//会阻塞 socket是来自客户端的连接 Log.println("有客户端连接上来了"); // 对方信息: InetAddress inetAddress = socket.getInetAddress(); // ip Log.println("对方的 ip: " + inetAddress); int port = socket.getPort(); // port Log.println("对方的 port: " + port); SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress(); // ip + port Log.println("对方的 ip + port: " + remoteSocketAddress); // 读取请求 InputStream inputStream = socket.getInputStream(); Scanner scanner = new Scanner(inputStream, "UTF-8"); String request = scanner.nextLine(); // nextLine() 就会去掉换行符 String engWord = request; Log.println("英文: " + engWord); // 翻译 String chiWord = translate(engWord); Log.println("中文: " + chiWord); // 发送响应 String response = chiWord; // TODO: 响应的单词中是没有 \r\n OutputStream outputStream = socket.getOutputStream(); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8"); PrintWriter writer = new PrintWriter(outputStreamWriter); Log.println("准备发送"); writer.printf("%s\r\n", response); writer.flush(); Log.println("发送成功"); // 挂掉电话 关闭在循环里面,说明每次发送一个信息就会关闭连接 socket.close(); Log.println("挂断电话"); } // serverSocket.close(); } private static final HashMap<String, String> map = new HashMap<>(); private static void initMap() { map.put("apple", "苹果"); map.put("pear", "梨"); map.put("orange", "橙子"); } private static String translate(String engWord) { String chiWord = map.getOrDefault(engWord, "查无此单词"); return chiWord; } }
客户端
package com.lsc.net_demo.tcp; import com.lsc.net_demo.util.Log; import java.io.*; import java.net.Socket; import java.util.Scanner; public class UserInputLoopConnectionClient { //短连接 public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); while (true) { System.out.print("请输入英文单词"); if(!scanner.hasNextLine()){ break; } String engWord = scanner.nextLine(); //直接创建Socket 使用服务器IP+PORT Log.println("准备创建socket(TCP连接)"); Socket socket = new Socket("127.0.0.1", TranslateServerShortConnection.PORT); Log.println("创建socket(TCP连接)成功创建"); //发送请求 Log.println("英文" + engWord); String request = engWord + "\r\n"; OutputStream os = socket.getOutputStream(); OutputStreamWriter osWriter = new OutputStreamWriter(os); PrintWriter pw = new PrintWriter(osWriter); Log.println("准备发生请求"); pw.print(request); pw.flush(); Log.println("发送请求成功"); //等待响应接收 InputStream is = socket.getInputStream(); Scanner sc = new Scanner(is, "UTF-8"); Log.println("准备接收请求"); String chiWord = sc.nextLine(); Log.println("接收请求成功"); Log.println("中文" + chiWord); socket.close();//每次发送一个信息,都会关闭连接 然后循环重新连接 } } }
长链接
服务器(带多线程解决不是同时在线问题)
package com.lsc.net_demo.tcp; import com.lsc.net_demo.util.Log; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; import java.util.HashMap; import java.util.Scanner; public class TranslateServerLongConnectionPool { static class 专门负责处理接电话的人 implements Runnable{ Socket socket; public 专门负责处理接电话的人(Socket socket){ this.socket=socket; } @Override public void run() { try { // 对方信息: InetAddress inetAddress = socket.getInetAddress(); // ip Log.println("对方的 ip: " + inetAddress); int port = socket.getPort(); // port Log.println("对方的 port: " + port); SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress(); // ip + port Log.println("对方的 ip + port: " + remoteSocketAddress); //初始化 //用来接收信息 InputStream inputStream = socket.getInputStream(); Scanner scanner = new Scanner(inputStream, "UTF-8"); //用来发送信息 OutputStream outputStream = socket.getOutputStream(); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8"); PrintWriter writer = new PrintWriter(outputStreamWriter); while (true) { if(!scanner.hasNextLine()) { Log.println("对方挂电话了"); //代表scanner背后的InputStream遇到了EOS(-1) //代表客户端挂电话了 //所以服务端也挂电话 break; } // 读取请求 String request = scanner.nextLine(); // nextLine() 就会去掉换行符 String engWord = request; Log.println("英文: " + engWord); // 翻译 String chiWord = translate(engWord); Log.println("中文: " + chiWord); // 发送响应 String response = chiWord; // TODO: 响应的单词中是没有 \r\n Log.println("准备发送"); writer.printf("%s\r\n", response); writer.flush(); Log.println("发送成功"); } // 挂掉电话 socket.close(); Log.println("挂断电话"); }catch (Exception e){ e.printStackTrace(); } } } public static final int PORT = 8888; public static void main(String[] args) throws Exception { Log.println("启动长链接版本的TCP服务器"); initMap(); ServerSocket serverSocket = new ServerSocket(PORT); while (true) { // 接电话 Log.println("等待对方来连接"); Socket socket = serverSocket.accept();//会阻塞 socket是来自客户端的连接 Log.println("有客户端连接上来了"); Runnable task=new 专门负责处理接电话的人(socket); //把任务交给专门的线程处理 new Thread(task).start(); } // serverSocket.close(); } private static final HashMap<String, String> map = new HashMap<>(); private static void initMap() { map.put("apple", "苹果"); map.put("pear", "梨"); map.put("orange", "橙子"); } private static String translate(String engWord) { String chiWord = map.getOrDefault(engWord, "查无此单词"); return chiWord; } }
客户端
package com.lsc.net_demo.tcp; import com.lsc.net_demo.util.Log; import java.io.*; import java.net.Socket; import java.util.Scanner; public class UserInputLoopLongConnectionClient { public static void main(String[] args) throws IOException { //长链接模式下,只拨通一次电话 //直接创建Socket 使用服务器IP+PORT Scanner scanner=new Scanner(System.in); Log.println("准备创建socket(TCP连接)"); Socket socket=new Socket("127.0.0.1",TranslateServerShortConnection.PORT); Log.println("创建socket(TCP连接)成功创建"); while (true) { System.out.print("请输入英文单词"); if(!scanner.hasNextLine()){ break; } String engWord = scanner.nextLine(); String request = engWord + "\r\n"; OutputStream os = socket.getOutputStream(); OutputStreamWriter osWriter = new OutputStreamWriter(os); PrintWriter pw = new PrintWriter(osWriter); Log.println("准备发生请求"); pw.print(request); pw.flush(); Log.println("发送请求成功"); //等待响应接收 InputStream is = socket.getInputStream(); Scanner sc = new Scanner(is, "UTF-8"); Log.println("准备接收请求"); String chiWord = sc.nextLine(); Log.println("接收请求成功"); Log.println("中文" + chiWord); } socket.close();//只有在输入多个信息完毕之和,才会关闭连接 } }