Java|Java Socket -- 网络编程

Socket 套接字

  • 用于描述IP地址和端口
  • 为网络编程提供一种机制
  • 通信的两端都有Socket
  • 网络通信就是Socket间的通讯
  • 数据在两个Socket之间通过IO进行通讯
网络通信中的三个要素
  • IP地址:InetAddress (网络中设备的标识,不易记忆,可用主机名)
  • 端口号:用于标记进程的逻辑地址,不同进程的标识。(网络程序进程的地址)
  • 传输协议:通讯的规则,常见的协议:TCP/IP , UDP
协议
  • UDP协议
? 将数据源和目的封装数据包中,不需要建立连接。
? 每个数据包的大小限制在64K;
? 因无连接,是不可靠的协议
? 特点:不需要建立连接,速度快
  • TCP/IP协议
? 建立连接,形成传输数据的通道;
? 在连接中进行大数据量传输;
? 通过三次握手完成连接,是可靠协议;
? 特点:必须建立连接,效率会稍低,不过安全可靠
关于三次握手
  • 为什么要建立连接需要三次握手?
【Java|Java Socket -- 网络编程】? 首先非常明确的是两次握手是最基本的。第一次握手,客户端发了个连接请求消息到服务端,服务端收到信息后知道自己与客户端是可以连接成功的,但此时客户端并不知道服务端是否已经接收到了它的请求,所以服务端接收到消息后的应答,客户端得到服务端的反馈后,才确定自己与服务端是可以连接上的,这就是第二次握手。
  • 为什么需要第三次握手?
? 假设一下如果没有第三次握手,而是两次握手后我们就认为连接成功了,那么会发生什么?第三次握手是为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误。
? 譬如发起请求遇到类似这样的情况:客户端发出去的第一个连接请求由于某些原因在网络节点中滞留了导致延迟,直到连接释放的某个时间点才到达服务端,这是一个早已失效的报文,但是此时服务端仍然认为这是客户端的建立连接请求第一次握手,于是服务端回应了客户端,第二次握手。
? 如果只有两次握手,那么到这里,连接就建立了,但是此时客户端并没有任何数据要发送,而服务端还在傻傻的等候佳音,造成很大的资源浪费。所以需要第三次握手,只有客户端再次回应一下,就可以避免这种情况。
TCP/IP协议
  • 链路层 : 用于定义物理传输通达,通常是对某些网络连接设备的驱动协议,例如针对光纤,网线提供的驱动。
  • 网络层: 整个TCP/IP协议的核心,他主要用于将传输的数据进行分组,将分组好的数据发送到目标计算机或者网络。
  • 传输层: 主要使用网络程序进行通信,在进行网络通信时,可以采用TCP/IP协议,也可以采用UDP协议。
  • 应用层: 主要负责应用程序的协议,例如HTTP协议,FTP协议。
UDP协议收发数据 发送端:
package UDPDemo; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /** * 使用UDP协议发送数据 *1.创建发送端Socket对象(Socket也称为:套接字) *2.创建数据并打包 *3.发送数据 *4.释放资源 */ public class udpDemo { public static void main(String[] args) throws IOException{//DatagramSocket类用来发送和接收数据报包的套接字,启用 UDP协议广播发送。 //构造方法:DatagramSocket():创建Socket对象并随机分配端口号 //DatagramSocket(int port):创建Socket对象并指定端口号。//创建发送端Socket对象 DatagramSocket ds = new DatagramSocket(); //创建数据并打包 /* * 类 DatagramPacket : 此类表示数据报包 * - 数据 byte[] * - 设备的地址 ip * - 进程的地址 端口号 */ //数据 String s = "hello udp , im comming ! "; //创建一个字节数组,接受数据 byte[] bys = s.getBytes(); //把字符串转换成字节数组 //字节数组的长度 int length = bys.length; //发送给当前设备 InetAddress address = InetAddress.getByName("CHINA-20180816L"); //获取接收端的ip地址 //设置端口号 int port = 8888; //对方设备中的程序的端口号 //打包 DatagramPacket dp = new DatagramPacket(bys, length, address, port); //构造方法内的四个参数分别是:数据,数据的长度,对方设备地址,对方设备中的程序的端口号 //发送数据 ds.send(dp); //从此套接字(Socket)发送数据报包。 //释放资源 ds.close(); /* * 个人理解: * 发送数据步骤: *将数据转成字节数组并获取该数组的长度, *获取接收端的ip地址和端口号, *把数据,数据长度,接收端地址,接收端端接口四个作为参数传入DatagramPacket构造方法中打包。 *通过DatagramSocket的send方法发送这个数据报包。 *最后释放资源。 */ } }

接收端:
package UDPDemo; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /** * 使用UDP协议接收数据 *1.创建接收端Socket对象 *2.接收数据 *3.解析数据 *4.输出数据 *5.释放资源 * * @author Administrator */ public class updAcceptDemo { public static void main(String[] args) throws IOException { // 创建接收端Socket对象 DatagramSocket ds = new DatagramSocket(8888); //接收数据 //定义一个字节数组 byte[] bys = new byte[1024]; //定义数据包对象 DatagramPacket dp = new DatagramPacket(bys, bys.length); System.out.println(1); ds.receive(dp); //(阻塞)从此套接字接收数据报包。 System.out.println(2); //解析数据 //InetAddress.getAddress(); //获取发送端的ip对象 InetAddress address = dp.getAddress(); //返回某台机器的 IP 地址,此数据报将要发往该机器或者是从该机器接收到的。//byte[] getData(); byte[] data = https://www.it610.com/article/dp.getData(); //返回数据缓冲区//int getLength() int length = dp.getLength(); //获取具体的收到的数据的长度//输出数据 System.out.println("sender ----> "+address.getAddress()); System.out.println(new String(data , 0, length)); //释放资源 ds.close(); /* * 个人理解: *创建接收端对象,你把端口号作为参数传入。 *定义一个字节数组用来接收数据 *定义数据包对象,把字节数组名和长度作为参数传入构造函数。 *调用DatagramSocket的receive方法接受发送端传过来的数据报包,这个方法执行后,会产生阻塞,等待数据报包传过来。 *接收到传过来的数据包报后,放入缓冲区,得到数据长度 *调用InetAddress的getAddress()方法,获取此对象的原始 IP 地址 *把传过来的数据从原本的字节数组转成字符串并输出 *最后释放资源。 */ } }

注意事项:
  • 端口号错误,数据可以正常发送,不会出现异常,但是收不到数据。
  • 端口号不能重复,端口号是一个进程在内存中的地址,唯一的。
TCP/IP协议收发数据 发送端:
import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; /* * 用TCP协议发送数据 *1.创建发送端Socket对象(创建连接) *2.获取输出流对象 *3.发送数据 *4.释放资源 */ public class TcpipSendDemo { public static void main(String[] args) throws IOException { //创建一个Socket并将其连接到指定 IP 地址的指定端口号。 Socket s = new Socket(InetAddress.getByName("CHINA-20180816L"),10086); //获取输出流对象 OutputStream os = s.getOutputStream(); //数据 String str = "hello world"; //通过输出流对象调用write方法 os.write(str.getBytes()); //释放资源 s.close(); /* * 个人理解 *首先创建一个发送端对象,通过Socket的带参构造方法传入两个参数,一个是ip地址,一个是端口号。 *获取输出流对象,调用它的write方法将数据写出 */ } }

接收端:
import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /* * 使用TCP/IP协议接收数据 *1.创建接收端Socket对象 *2.监听(阻塞) *3.获取输入流对象 *4.获取数据 *5.输出数据 *6.释放资源 */ public class TcpipReceiveDemo { public static void main(String[] args) throws IOException { //创建接收端Socket对象 ServerSocket ss = new ServerSocket(10086); //监听(阻塞) Socket s = ss.accept(); //获取输入流对象 InputStream is = s.getInputStream(); //获取数据 byte[] bys = new byte[1024]; //用于存储独到的字节个数 int len = is.read(bys); //输出数据 System.out.println(new String(bys,0,len)); //释放资源 s.close(); } /* * 个人理解: *首先创建接收端的ServerSocket对象,传端口号作为参数。 *然后调用他的accept方法监听, *获取输入流对象,通过getInputStream()方法获取数据。 *创建字节数组,把输入流数据读入字节数组中, *输出数据 *最后释放资源 */ }

注意:
  • 如果IO是自己new出来的,需要自己手动调用close()方法释放资源。
  • 如果IO是通过Socket对象获取出来的,无需手动关闭IO,关闭Socket即可。
模拟用户登录案例
客户端loginClientTest
package com.itheima_04; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; /* * 模拟用户登陆--客户端 *职责: 1.接受账号密码的输入 *2.发送给服务器判断 *3.接收服务器发来的结果并输出 *(使用TCP协议 ) */ public class LoginClientTest { public static void main(String[] args) throws IOException { //第一步:创建Socket对象 Socket socket = new Socket("127.0.0.1",8082); //第二步:接受输入 Scanner sc = new Scanner(System.in); System.out.println("请输入账号:"); String username = sc.nextLine(); System.out.println("请输入密码:"); String password = sc.nextLine(); //第三步:获取输出流 //PrintWriter构造方法接收2个参数,一个是OutputStream,一个是布尔值 PrintWriter pw = new PrintWriter(socket.getOutputStream(), true); pw.println(username); pw.println(password); //获取输入流 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String result = br.readLine(); System.out.println(result); socket.close(); }}

服务器端LoginServerTest
package com.itheima_04; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.List; import com.itheima_05.User; import com.itheima_05.UserDB; /* * 模拟用户登录--服务器端 *职责: 1.接受客户端发来的消息 *2.对消息进行判断 *3.将判断结果返回 */ public class LoginServerTest { public static void main(String[] args) throws IOException { //第一步:创建Socket对象 ServerSocket ss = new ServerSocket(8082); //第二步:监听 Socket accept = ss.accept(); //第三步:BufferedReader是一个缓冲区,还需要字符输入流,InputStreamReader构造方法的参数是InputStream类型 BufferedReader br = new BufferedReader(new InputStreamReader(accept.getInputStream())); //第四步:读取数据 String username = br.readLine(); String password = br.readLine(); //第五步:判断 boolean flag = false; //if("itheima".equals(username)&&"123456".equals(password)) { //flag = true; //}//改进 List users = UserDB.getUsers(); User user = new User(username,password); if(users.contains(user)) { flag = true; } //第六步:获取输出流 PrintWriter pw = new PrintWriter(accept.getOutputStream(),true); if(flag) { pw.println("登陆成功!"); }else{ pw.println("登录失败"); } accept.close(); }}

用户类User
package com.itheima_05; public class User { private String username; private String password; public User() {} public User(String username, String password) { super(); this.username = username; this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; }@Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; User other = (User) obj; if (password == null) { if (other.password != null) return false; } else if (!password.equals(other.password)) return false; if (username == null) { if (other.username != null) return false; } else if (!username.equals(other.username)) return false; return true; } }

用户数据库类UserDB
package com.itheima_05; import java.util.ArrayList; import java.util.List; public class UserDB { private static List users = new ArrayList<>(); static { users.add(new User("it001","123456")); users.add(new User("it002","aaaaaa")); users.add(new User("it003","111111")); }public static List getUsers() { return users; }public static void setUsers(List users) { UserDB.users = users; } }

    推荐阅读