17 Java网络编程
网络的基本知识
什么是网络
提供以下一些主要功能
-1、资源共享。
-2、信息传输与集中处理。
-3、均衡负载与分布处理。
-4、综合信息服务。
均衡负载是什么意思呢、画图例子:
均衡负载有个好处、比如服务器A挂掉、还可以使用其他服务器、保证项目正常运行。
网络分类
1. 局域网(LAN):
- 指在一个较小地理范围内的各种计算机网络设备互连在一起的通信网络、可以包含一个或多个子网、通常局限在几千米的范围之内。
2.城域网 (MAN) :
-主要是由城域范围内的各局域网之间互连而构成的、现在很少提起这个概念。
3.广域网(WAN):
- 是由相距较远的局域网或城域网互连而成、通常是除了计算机设备以外、还要涉及一些电信通讯方式。
网络分层
网络分层主要是分为两种模型、第一种:OSI理论推荐分层模型;第二种:TCP\IP的实际模型
OSI理论推荐分层模型
1. 应用层:
最顶层、主要是为了实现不同的业务而开发的各种协议、比如HTTP/FTP/SSH等、程序员往往比较关系这个层面的协议
2. 表示层:
主要用于处理两个通信系统中交换信息的表示方式、比如加密、压缩、格式转换等。
3. 会话层:
在两个节点之间建立连接
4. 传输层:
为会话层用户提供一个端到端的可靠、透明和优化的数据传输服务机制
5. 网络层:
建立节点的联系、路由选择、建立和维护连接、拥塞控制、计费、通常是交换机。
6. 数据链路层:
将数据分帧、处理流控制、差错控制、指定拓扑结构、硬件寻址、网卡、网桥、交换机
7. 物理层
传输电子信号、集线器、中继器、调制解调器、网线、双绞线、同轴电缆
TCP\IP的实际模型
TCP\IP的实际模型分为4层:1.应用层 2. 传输层 3. 网络层 4. 物理+数据链路
总结:两种模型其实都是相似的,只是OSI分层比较细,对比图如下:
IP地址
IP地址分成IPV4和IPV6,在IPV4里面,IP地址用32位数字来表示,为了方便记忆,通常划分为4个8位的二进制数。而8位二进制数可表示的数字范围是0-255。(8个二进制1 = 255,所以最大是255)
最新的IPv6则使用128位的数字来表示地址,可以容纳更多的网络设备。但是无论如何,IP地址都是一个特别难记的东西,为此出现了DNS。
物理地址:每一台电脑都会有一个物理地址,物理地址不可更改。
查看IP地址:win+r快捷键 -> 输入cmd -> 输入ipconfig/all
Ping:用于拼外网网络地址或局域网网络地址,如果当前计算机没有网络是ping不通的。
DNS域名系统
概念:域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。
图例子:
补充:localhost表示本机 ,127.0.0.1也表示本机,访问自己部署在自己电脑(服务器)上的项目时,可用localhost或者127.0.0.1进行访问项目。
总结:DNS域名系统 就是把域名(网址)解析成IP地址来进行通信/资源共享。
端口
端口三大类
公认端口:0 ~ 1023之间,在一些操作系统必须管理员才能申请这些端口
常用公认端口:
21: FTP协议,用于文件传输
22:SSH协议,用于安全的远程连接
25:SMTP,简单邮件传输协议,用浏览器到电商买东西使用就是此协议
注册端口: 1024 ~ 49151,松散绑定一些服务
比如Tomcat服务器,默认端口号8080
Oracle数据库,默认端口号1521
Mysql数据库,默认端口号3306
动态端口/私有端口:49152 ~ 65535 ,应用程序使用的动态端口,一般应用程序不主动使用它
套接字ServerSocket和Socket
- Socket客户端套接字
- ServerSocket服务端套接字
- ServereSocket对象用于监听来自客户端的Socket连接,如果没有连接,它将一直处于等待状态。ServerSocket包含一个监听来自客户端连接请求的方法,accept方法。
- ServerSocket accept():如果接收到一个客户端Socket的连接请求,该方法将返回一个与客户端Socket对应的Socket;否则该方法将一直处于等待状态,线程也被阻塞。
例子1 客户端和服务端简单的交互:
//服务端代码 import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /** * 服务端ServerSocket版本1,服务端只可以接收客户端的消息,不能发消息给客户端 * @author leak * */ public class ServerSocketT1 { public static void main(String[] args) { try { // 1. 创建ServerSocket(服务端套接字),并给当前项目指定端口号,注意指定的端口号不可以使用常用的端口号,会出现端口占用 ServerSocket server = new ServerSocket(81); System.out.println("服务端创建成功,等待连接"); Socket socket = server.accept();//等待一个连接,如果没有客户端连接就一直处于阻塞状态(等待) System.out.println("连接服务端成功"); //获取客户端的输入流 InputStream inputStream = socket.getInputStream(); //因为客户端那边使用了数据流,所以服务端这边也得使用数据流 DataInputStream data = new DataInputStream(inputStream); //这里的readUTF()直接输出字符,不需要再转换为字符流 System.out.println("客户端发送的信息:"+data.readUTF()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } //客户端代码 import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; /** * 客户端Socket1,注意要先运行服务端,才可以运行客户端 * 当前客户端版本1,只能发送消息给服务端,不可以接收服务端的消息 * @author leak * */ public class Client1 { @SuppressWarnings("resource") public static void main(String[] args) { String ip = "localhost"; //这里的ip也可以是 域名(网址) try { //创建Socket进行连接服务端 Socket socket = new Socket(ip,81);//连接对应的服务端,需要ip和端口,如果这个ip和端口不存在,则连接失败,成功则连接成功 //根据套接字获取输出流,把客户端的信息输出给服务端 OutputStream out = socket.getOutputStream(); //因为输入字符,需要把字节转字符,而且读取要循环,有点麻烦,所以使用数据流 DataOutputStream dataOutputStream = new DataOutputStream(out); //数据流的writeUTF(字符串)可以直接写入字符串,当然以数据流输出的,服务端也要使用数据流接收,否则报异常(输入出流不对应) dataOutputStream.writeUTF("hello 服务器"); //刷新缓存,确保消息发送出去 dataOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } } }
注意:版本1这里的服务端只能接收客户端发送过来的消息,而不可以发送消息给客户端;相反客户端只可以发送消息给服务端,但不可以接收服务端的消息。
例子2 客户端和服务端相互之间接收发消息(只能发送一次)。
//服务端版本2 import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /** * 服务端版本2 * 版本1只能接收客户端的信息,版本2可以发信息给客户端 * @author leak * */ public class ServerSocket2 { public static void main(String[] args) { //初始化 Socket socket = null; ServerSocket server = null; DataOutputStream dataOut = null; DataInputStream dataIn = null; try { // 创建服务端,并给当前服务端指定端口号 server = new ServerSocket(82); //等待客户端连接 socket = server.accept(); //数据流接收数据 dataIn = new DataInputStream(socket.getInputStream()); System.out.println("客户端发送的信息:"+dataIn.readUTF()); //创建输出流 dataOut = new DataOutputStream(socket.getOutputStream()); dataOut.writeUTF("服务端发送给客户"+socket+"的消息: 你好啊!"); dataOut.flush(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { try { //关闭输入出流 dataOut.close(); dataIn.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } //客户端版本2 import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; import java.util.Scanner; /** * 客户端版本2 * 客户端2除了可以发送消息给服务端,可以接收服务端的消息 * * @author leak * */ public class Client2 { public static void main(String[] args) { // 创建客户端套接字 String ip = "localhost"; // 初始化 Socket socket = null; DataOutputStream dataOut = null; DataInputStream dataIn = null; Scanner scanner = null; try { // 这里创建套接字成功代表,已经连接上服务端了 socket = new Socket(ip, 82);// 这里指定的是版本2的服务端 端口号 // 创建输出流 dataOut = new DataOutputStream(socket.getOutputStream()); // 创建输入流 dataIn = new DataInputStream(socket.getInputStream()); // 创建手动输入流 scanner = new Scanner(System.in); System.out.println("请输入发送给服务端的信息:"); // 用户输入内容 String text = scanner.next(); // 把用户的输入的内容放进输出流,发送给服务端 dataOut.writeUTF(text); dataOut.flush(); // 接收服务端发送过来的消息 System.out.println(dataIn.readUTF()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { // 关闭输入出流 dataOut.close(); dataIn.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
注意:版本2之间可以相互发送消息,但是因为不同的两个程序运行,所以有两个控制台,所以不能显示客户端和服务端互发的消息。但是相互之间发送的消息可以切换控制台查看,控制台地方如下图。
例子3 客户端和服务端可以一直互相发送消息。
//服务端版本3 import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; /** * 服务端版本3 版本3能一直接收客户端的信息,版本3可以一直发信息给客户端 * * @author leak * */ public class ServerSocket3 { public static void main(String[] args) { // 初始化 Socket socket = null; ServerSocket server = null; DataOutputStream dataOut = null; DataInputStream dataIn = null; Scanner scanner = null; try { // 创建服务端,并给当前服务端指定端口号 server = new ServerSocket(82); // 等待客户端连接 socket = server.accept(); scanner = new Scanner(System.in); // 循环等待接收消息,注意上面3行代码为什么不放进循环呢? // 因为现在的需求是一个客户端,一个服务端,而且如果new ServerSocket(82)放进循环会出现端口占用情况, // 而server.accept()会创建多个客户端等待连接,现在只需要一个客户端,所以不用放进循环, //Scanner是创建输入流,创建一次就可以使用,不用重复创建,浪费资源,因为没有关闭输入流 while (true) { // 数据流接收数据 dataIn = new DataInputStream(socket.getInputStream()); System.out.println("客户端发送的信息:" + dataIn.readUTF()); // 创建输出流 dataOut = new DataOutputStream(socket.getOutputStream()); dataOut.writeUTF("服务端发送给客户" + socket + "的消息:"+scanner.next()); dataOut.flush(); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { // 关闭输入出流 dataOut.close(); dataIn.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } //客户端版本3 import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; import java.util.Scanner; /** * 客户端版本3 客户端3可以一直发送消息给服务端,一直接收服务端的消息 * * @author leak * */ public class Client3 { public static void main(String[] args) { // 创建客户端套接字 String ip = "localhost"; // 初始化 Socket socket = null; DataOutputStream dataOut = null; DataInputStream dataIn = null; Scanner scanner = null; try { // 这里创建套接字成功代表,已经连接上服务端了 socket = new Socket(ip, 82);// 这里指定的是版本2的服务端 端口号 // 创建手动输入流 scanner = new Scanner(System.in); //上面2行代码不放进循环是因为,new Socket(ip, 82)是连接服务端的,连接一次就可以进行消息传输,所以不必重复, //而new Scanner()是创建输入流,创建一次就可以输入消息了 while (true) { // 创建输出流 dataOut = new DataOutputStream(socket.getOutputStream()); // 创建输入流 dataIn = new DataInputStream(socket.getInputStream()); System.out.println("请输入发送给服务端的信息:"); // 用户输入内容 String text = scanner.next(); // 把用户的输入的内容放进输出流,发送给服务端 dataOut.writeUTF(text); dataOut.flush(); // 接收服务端发送过来的消息 System.out.println(dataIn.readUTF()); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { // 关闭输入出流 dataOut.close(); dataIn.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
因为互发消息,需要来回切换控制台才可以看到消息,就是上面版本2图片一样,要来回切换控制台,所以现在采用cmd运行程序,因为cmd可以开多个窗口,所以把代码使用cmd编译和运行(提示:javac 类名.java 是编译,java 类名 是运行)。注意:直接在桌面创建txt文件,然后把代码复制进去,改后缀名,然后用cmd编译运行就可以(注意一下txt编码格式,因为cmd默认是GBK,如果txt是UTF-8会保错,建议txt使用ANSI编码格式保存)
可以发现客户端和服务端可以一直互相发送消息,但是有一个缺点,就是服务端只能先接收消息,才能发消息,而且服务端发消息给客户端后,不能连续发消息给客户端,要等客户端回复消息才可以继续发消息给客户端(类似回合制游戏一样,你打我一下,我打你一下,不能连续打)
这是代码的原因(输入和输出流处理都是混杂在一起),服务端循环体的代码是先执行socket.getInputStream()等待客户端的消息,处于阻塞状态,所以不能执行下面的输入语句,而客户端那边是现在执行scanner.next()等待输入,所以不能执行下面的输出语句。导致客户端和服务端交互,只能一发一收,不能连续发消息,下面版本4解决这个问题。
版本4 解决客户端和服务端只能一发一收的问题,实现连续发消息,连续收消息。
代码:
//服务端版本4 import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; /** * 服务端版本4 版本4能一直连续接收客户端的信息,版本4可以一直连续发信息给客户端 * 主线程接收客户端的信息 * 子线程发送给客户端的信息 * @author leak * */ public class ServerSocket4 { public static void main(String[] args) { // 初始化 Socket socket = null; ServerSocket server = null; DataInputStream dataIn = null; try { // 创建服务端,并给当前服务端指定端口号 server = new ServerSocket(82); // 等待客户端连接 socket = server.accept(); // 循环等待接收消息,注意上面2行代码为什么不放进循环呢? // 因为现在的需求是一个客户端,一个服务端,而且如果new ServerSocket(82)放进循环会出现端口占用情况, // 而server.accept()会创建多个客户端等待连接,现在只需要一个客户端,所以不用放进循环, // 开启子线程处理输出流 new Thread(new ServerSocket4Thread(socket)).start(); //主线程是处理输入流 while (true) { // 数据流接收数据 dataIn = new DataInputStream(socket.getInputStream()); System.out.println("客户端发送的信息:" + dataIn.readUTF()); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { // 关闭输入流 dataIn.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } class ServerSocket4Thread implements Runnable { private Socket socket; private DataOutputStream dataOut = null; private Scanner scanner = new Scanner(System.in); //无参构造 public ServerSocket4Thread() { // TODO Auto-generated constructor stub } //因为分开处理输入出流,但是都依赖于socket对象,所以这里需要主线程把对象传递过来 public ServerSocket4Thread(Socket socket) { this.socket = socket; } @Override public void run() { try { //子线程循环处理输出流 while (true) { // 创建输出流 dataOut = new DataOutputStream(socket.getOutputStream()); System.out.println("请输入发送给客户端的消息:"); dataOut.writeUTF("服务端发送给客户" + socket + "的消息:" + scanner.next()); dataOut.flush(); } } catch (Exception e) { e.printStackTrace(); } finally { try { // 关闭输出流 dataOut.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } //客户端版本4 import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; import java.util.Scanner; /** * 客户端版本4 客户端4可以一直连续发送消息给服务端,一直连续接收服务端的消息 因为版本3是把输入出流处理混杂在一起,导致了只能回合制 * 现在版本4采用多线程,输入出流分开处理 主线程发送给服务端的消息 * * @author leak * */ public class Client4 { private static Scanner scanner = null; public static void main(String[] args) { scanner = new Scanner(System.in); // 创建客户端套接字 String ip = "localhost"; // 初始化 Socket socket = null; DataOutputStream dataOut = null; try { // 这里创建套接字成功代表,已经连接上服务端了 socket = new Socket(ip, 82);// 这里指定的是版本2的服务端 端口号 // 上面2行代码不放进循环是因为,new Socket(ip, 82)是连接服务端的,连接一次就可以进行消息传输,所以不必重复, // 这里开启子线程是因为在死循环下面是无法执行的语句 new Thread(new Client4Thread(socket)).start(); while (true) { // 创建输出流 dataOut = new DataOutputStream(socket.getOutputStream()); System.out.println("请输入发送给服务端的信息:"); // 用户输入内容 String text = scanner.next(); // 把用户的输入的内容放进输出流,发送给服务端 dataOut.writeUTF(text); dataOut.flush(); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { // 关闭输入出流 dataOut.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } //子线程接收服务端的信息 class Client4Thread implements Runnable { private Socket socket = null; private DataInputStream dataIn = null; public Client4Thread() { } public Client4Thread(Socket socket) { this.socket = socket; } @Override public void run() { try { // 循环接受服务端的消息 while (true) { // 创建输入流 dataIn = new DataInputStream(socket.getInputStream()); // 接收服务端发送过来的消息 System.out.println(dataIn.readUTF()); } } catch (Exception e) { e.printStackTrace(); } finally { try { // 关闭输入出流 dataIn.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
版本4采用了多线程方式,把输入出流分开处理,实现连续收发消息。
版本5实现群聊,版本4只能2个人互相交流,如果是多个人交流呢,还有群聊是共享信息的,也就是一个人发消息,其他人都可以看得到消息。
代码:
//服务端版本5,实现群聊 import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List; /** * 服务端版本5,实现群聊功能,多个客户端互相交流 实现思路:服务端开启,每一个客户端加入 就开启一个线程, * 客户端发送消息给服务端,服务端就把该客户端的消息发布给每一个客户端实现消息共享(群聊功能) * * @author leak * */ public class ServerSocket5 { // 这个是用来统计客户端在线人数,为什么用list集合呢,因为取出socket的时候根据下标取比较方便, //如果是用map集合要根据key取,比较难,因为key是客户0,客户1这样的,所以如果一定要用map集合存储, //那么取的时候,就要把key字符串分隔成 客户 和 数字 ,然后把数字转整型 再循环+1,然后再拼接成字符串,就可以根据key取value了 public final static List<Socket> sockets = new ArrayList<Socket>(); static int i = 0;//给客户端起序号 public static void main(String[] args) { try { // 1、开启服务端并指定端口号 ServerSocket server = new ServerSocket(98); System.out.println("服务端创建成功,等待客户端连接,服务端等待期间处于阻塞状态"); while (true) { // 2、创建套接字等待客户端连接,放进循环代表,可以获取多个客户端的连接 Socket socket = server.accept();// 每循环一次,就是一个不同的客户端 // 3、统计在线人数 String name = "客户" + i;// 客户端名字 // 把客户端添加进map集合统计在线人数 sockets.add(socket); System.out.println("当前在线人数:" + sockets.size()); i++; // 4、每获取一个客户端,就开启一个线程处理 new Thread(new ServerSocket5Run(socket), name).start(); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } class ServerSocket5Run implements Runnable { private Socket socket = null; private DataInputStream dataIn = null; private DataOutputStream dataOut = null; // 无参构造 public ServerSocket5Run() { } public ServerSocket5Run(Socket socket) { this.socket = socket; } @Override public void run() { try { while(true) { //获取每一个客户端的输出流 //获取当前客户端发送过来的消息 dataIn = new DataInputStream(socket.getInputStream()); String user = Thread.currentThread().getName(); String message = user+"说:"+dataIn.readUTF(); //循环每一个客户端,把消息发给他们 for(int i = 0 ; i < ServerSocket5.sockets.size(); i++) { Socket per_Socket = ServerSocket5.sockets.get(i);//获取map集合里面的每个客户端 dataOut = new DataOutputStream(per_Socket.getOutputStream()); dataOut.writeUTF(message); dataOut.flush(); } } } catch (Exception e) { e.printStackTrace(); } } } //客户端代码和版本4一样,可以直接拿版本4的客户端代码直接用,因为版本5只改动了服务端的代码
版本5因为服务端不用在手动输入信息回复,只做转发处理,就是把其中一个客户端的消息转发到每一个客户端。
版本5效果图如下: