JAVA网络编程

1. 网络概述

  • 什么是网络?

    由点和线构成表示诸多对象间的互相联系。在生活当中,人际关系是一种网络,足球网是一种网络,蜘蛛网也是一种网络。

1.1 计算机网络

  • 什么是计算机网络?

    为实现资源共享和信息传递,通过通信线路连接起来的若干主机(Host)。比如多个独立的主机/电脑通过双绞线或者光纤连接起来就形成了计算机网络。

  • 按照地理范围网络分为:

    • 局域网。

      范围比较小,可能是一间教室,一栋大楼或者一个校区。

    • 地域网。

      可以是一个或者多个城市形成的网络。

    • 广域网。

      最大的网络,又可以分为:

      • 互联网:(Internet)点与点相连。
      • 万维网:(WWW - World Wide Web)端与端相连。
      • 物联网:(IoT - Internet of things)物与物相连。
  • 网络编程:

    让计算机与计算机之间建立连接进行通信

2. 网络模型

2.1 OSI参考模型

  • OSI(Open System Interconnection)开放式系统互联。

这个模型把整个网络的建设分成了七层,下层为上层服务:

  • 第七层:应用层负责文件访问和管理、可靠运输服务、远程操作服务。(HTTP、FTP、SMTP)
  • 第六层:表示层负责定义转换数据格式及加密,允许选择以二进制或ASCII格式传输。
  • 第五层:会话层负责使应用建立和维持会话,使通信在失效时继续恢复通信。(断点续传)
  • 第四层:传输层负责是否选择差错恢复协议、数据流重用、错误顺序重排。(TCP、UDP)
  • 第三层:网络层负责定义了能够标识所有网络结点的逻辑地址。(IP地址)
  • 第二层:链路层在物理层上,通过规程或者协议(差错控制)来控制传输数据的正确性。(MAC)
  • 第一层:物理层为设备之间的数据通信提供信号和物理介质。(双绞线、光导纤维)

举个实际例子,假设我在QQ上给我的学妹发了条“我喜欢你”,这条信息会从应用层向下到物理层进行传递,每下一层都会包裹某种协议,最后在物理层转换为数字信号或者光信号等通过网卡或者路由器等传递到学妹端的物理层,这种信号又从物理层往上到应用层,学妹就能看见我发了条信息,学妹也能以这样的方式给我回复消息,有可能是“你是个好人”等其他信息,这是一个双向的传递。在这个过程中,涉及到各层的协议,协议实际就是约定数据传输的格式,我发送了中文,别人也一定看到的是同样的内容。

2.2 TCP/IP模型

  • 一组用于实现网络互连的通信协议,将协议分为四个层次。

由于OSI参考模型采用的是七层的建设模型,比较复杂,所以在网络建设中一般采用的是TCP/IP模型。实际上TCP/IP模型的四层也对应这OSI模型的七层,它将OSI的五到七层合成了应用层,把一二层合成了网络接口层。

  • 第四层:应用层负责传送各种最终形态的数据,是直接与用户打交道的层,典型协议是HTTP、FTP等。
  • 第三层:传输层负责传送文本数据,主要协议是TCP、UDP协议。
  • 第二层:网络层负责分配地址和传送二进制数据,主要协议是IP协议。
  • 第一层:接口层负责电路连接,是整个网络的物理基础,典型的协议包括以太网、ADSL等等。

3. 通信协议

3.1 TCP协议

  • TCP协议:Transmission Control Protocol 传输控制协议

  • 是一种面向连接的可靠的面向字节流的传输层通信协议。数据大小无限制。建立连接的过程需要三次握手,断开连接的过程需要四次挥手。

  • 面向连接的意思就是两台计算机(应用程序)在通信之前,必须先建立连接。

  • TCP的三次握手,或者说三报文握手,是TCP建立连接过程的机制。主动发起TCP连接的应用进程叫做客户,而被动等待连接建立的应用程序叫做服务器。客户向服务器发送请求后,服务器要确认客户的连接请求,然后客户要对服务器的确认进行确认。

  • TCP的四次挥手,或者说四报文握手,是TCP释放连接过程的机制。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后就进入了半关闭状态。当另一方也没有数据再发送时,则发送连接释放通知,对方确认后就完全关闭了TCP连接。

  • “面向字节流”的含义是:虽然应用程序和TCP的交互试一次一个数据块(大小不等),但TCP把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。TCP并不知道所传送的字节流的含义。TCP不保证接收方应用程序所收到的数据块和发送方应用程序所发出的数据块具有对应大小的关系(例如,发送方应用程序交给发送方的TCP共10个数据块,但接收方的TCP可能只用了4个数据块就把收到的字节流交付给上层的应用程序)。但接收方的应用程序收到的字节流必须和发送方应用程序发出的字节流完全一样。

3.2 UDP协议

  • UDP协议:User Datagram Protocol用户数据报协议
  • 是一种无连接的传输层协议,提供面向报文的简单不可靠信息传送服务,每个包的大小是64KB。
  • 无连接是指UDP在传送数据之前不需要先建立连接。远地主机的运输层在收到UDP报文后,不需要给出任何确认。虽然UDP不提供可靠交付,但在某些情况下UDP却是一种最有效的工作方式。
  • “面向报文”简单说就是应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。也就是说,UDP一次交付一个完整的报文。

3.3 IP协议

IP协议:Internet Protocol 互联网协议/网际协议

  • 负责数据从一台机器发送到另一台机器。
  • 给互联网每台设备分配一个唯一标识(IP地址)。

IP地址分为两种:

  • IPV4:4字节32位整数,并分成4段8位的二进制数,每8位之间用圆点隔开,每8位整数可以转换为一个0~255的十进制整数。

    格式:D.D.D.D 例如:255.255.255.255

    IPV4是上个世纪70年代出现的一个版本,到现在差不多五十年,所以面临着一个资源耗尽的问题。在上个世纪90年代出现了IP的第6个版本,据说IPV6可以给地球的每一个沙子分配一个IP。

  • IPV6:16字节128位整数,并分成8段十六进制数,每16位之间用圆点隔开,每16位整数可以转换为一个0~65535的十进制数。

    格式:X.X.X.X.X.X.X.X 例如:FFFF.FFFF.FFFF.FFFF.FFFF.FFFF.FFFF.FFFF

    这里的4和6是IP的第4个和第6个版本,版本1~3和版本5都未曾使用过。

IPV4的应用分类:

  • A类:政府机构,1.0.0.1~126.255.255.254

  • B类:中型企业,128.0.0.1~191.255.255.254

  • C类:个人用户,192.0.0.1~223.255.255.254

  • D类:用于组播,224.0.0.1~239.255.255.254

  • E类:用于实验,240.0.0.1~255.255.255.254

  • 回环地址:127.0.0.1,指本机,一般用于测试使用。

  • 测试IP命令:ping D.D.D.D

    比如你可以打开cmd,输入ping www.baidu.com

  • 查看IP命令:ipconfig

3.4 Port端口

  • 端口号:在通信实体上进行网络通讯程序的唯一标识。

    举个例子,我用IP地址为192.168.0.1的电脑上的QQ程序,给学妹的IP地址为192.168.0.2的电脑上的QQ发送一条信息“我喜欢你”,而且我还用电脑上的微信发送了相同的信息给学妹电脑上的微信,那么这两条信息发送的IP地址都是学妹的电脑192.168.0.2,如何保证QQ的消息不会发送到微信上,不至于出现混乱?所以,程序在发送信息的时候,不仅要知道对方的IP地址,还要知道程序的端口号。

  • 端口分类(一般是两个字节):

    • 公认端口:0~1023

      像这样的端口号是被占用的,我们不能使用。

    • 注册端口:1024~49151

    • 动态或私有端口:49152~65535

      像这样的端口号是动态分配的,而且两个网络程序不可能出现端口号相同的情况,但是对于TCP/UDP协议,他们是两套端口号,它们端口号相同是不冲突的,每个协议都是独立的。

  • 常用端口:

    • MySql:3306
    • Oracle:1521
    • Tomcat:8080
    • SMTP:25
    • Web服务器:80
    • FTP服务器:21

4. 网络编程

4.1 InetAddress类

  • 概念:表示互联网协议(IP)地址对象,封装了与该IP地址相关的所有信息,并提供获取信息的常用方法。

  • 方法:

    • public static InetAddress getLocalHost()

      获得本地主机地址对象。

    • public static InetAddress getByName(String host)

      根据主机名称获得地址。

    • public static InetAddress[] getAllByName(String host)

      获得所有相关地址对象。

    • public String getHostAddress()

      获取IP地址字符串

    • public String getHostName()

      获得IP地址主机名。

演示InetAddress类的使用:

  1. 创建本机IP地址对象。

    //1. getLocalhost()方法
    InetAddress ia1=InetAddress.getLocalHost();
    System.out.println("ip地址:"+ia1.getHostAddress()+" 主机名:"+ia1.getHostName());
    

    输出结果如下,这是我的IP地址和电脑主机名:

    ip地址:172.16.0.133 主机名:LAPTOP-BDJQBB64
    

    还有其他方式,不过一般情况下经常使用的是上面一种和第二种方法:

    //2. getByName("ip地址")
    InetAddress ia2=InetAddress.getByName("LAPTOP-BDJQBB64");
    System.out.println("ip地址:"+ia2.getHostAddress()+" 主机名:"+ia2.getHostName());
    //3. getByName("127.0.0.1")
    InetAddress ia3=InetAddress.getByName("127.0.0.1");
    System.out.println("ip地址:"+ia3.getHostAddress()+" 主机名:"+ia3.getHostName());
    //4. getByName("localhost")
    InetAddress ia4=InetAddress.getByName("localhost");
    System.out.println("ip地址:"+ia4.getHostAddress()+" 主机名:"+ia4.getHostName());
    

    输出结果如下:

    ip地址:172.16.0.133 主机名:LAPTOP-BDJQBB64
    ip地址:127.0.0.1 主机名:servserv.generals.ea.com
    ip地址:127.0.0.1 主机名:localhost
    
  2. 创建局域网IP地址对象

    也就是查找说和我这台电脑位于同一个网络中的电脑。

    InetAddress ia5=InetAddress.getByName("192.168.1.0");
    //判断指定的电脑/IP在指定时间里能否可达,参数为毫秒数
    //true表示可达,false表示不可达
    System.out.println(ia5.isReachable(2000));
    System.out.println("ip地址:"+ia5.getHostAddress()+" 主机名:"+ia5.getHostName());
    

    这个IP地址是我编的,是不可达的,所以结果的IP地址和主机名都返回了我输入的地址:

    false
    ip地址:192.168.1.0 主机名:192.168.1.0
    
  3. 创建外网IP地址对象

    InetAddress ia6=InetAddress.getByName("www.baidu.com");
    System.out.println("ip地址:"+ia6.getHostAddress()+" 主机名:"+ia6.getHostName());
    

    输出结果:

    ip地址:39.156.66.18 主机名:www.baidu.com
    

    还可以获取百度域名下的IP地址所组成的数组:

    InetAddress[] ia=InetAddress.getAllByName("www.baidu.com");
    for (InetAddress inetAddress : ia) {
        System.out.println(inetAddress.toString());
    }
    

    结果如下:

    www.baidu.com/39.156.66.14
    www.baidu.com/39.156.66.18
    

4.2 基于TCP协议的Socket网络编程

Socket编程:

  • Socket(套接字)是网络中的一个通信结点。

    每一条TCP连接有两个端点,TCP的端点叫做套接字(socket)或插口。根据RFC 793的定义:端口号拼接到(Concatenated with)IP地址构成了套接字。因此,套接字的表示方法是在点分之进制的IP地址后面写上端口号,中间用冒号或者逗号隔开。例如,若IP地址是192.3.4.5而端口号是80,那么得到的套接字就是(192.3.4.5:80)。总之,我们有

    套接字socket = ( IP地址 : 端口号 )

  • 分为客户端Socket和服务器ServerSocket。

  • 通信要求:IP地址+端口号。

4.2.1 开发步骤

服务器端步骤:

  • 创建ServerSocket,指定端口号。
  • 调用accept等待客户端接入。
  • 使用输入流接受请求数据到服务器(等待)。
  • 使用输出流发送响应数据给客户端。
  • 释放资源。

客户端步骤:

  • 创建Socket,指定服务器IP + 端口号。
  • 使用输出流发送请求数据到服务器。
  • 使用输入流接受响应数据到客户端(等待)。
  • 释放资源。

4.2.2 TCP编程案例一

  • TCP编程实现客户端发送数据给服务器端。
/**
 * TCP服务器端
 */
public class TcpServer {
	public static void main(String[] args) throws IOException {
//		1. 创建服务器套接字ServerSocket,绑定端口号。
		ServerSocket listener=new ServerSocket(6666);
//		2. 调用accept,侦听并接受连接到此服务器套接字的请求
        System.out.println("服务器已启动...");
		Socket socket=listener.accept();        
//		3. 使用输入流,接受请求数据到服务器(等待)。
		InputStream is=socket.getInputStream();
		//使用转换流,以接收中文数据
		BufferedReader br=new BufferedReader(new InputStreamReader(is,"utf-8"));
        System.out.println("客户发送:"+br.readLine());
//		4. 使用输出流,发送响应数据给客户端。【可选】
//		5. 释放资源。
		br.close();
		socket.close();
		listener.close();
	}
}

创建服务器套接字时可以绑定一个端口,如上述代码块中的6666,端口号范围可以是0~65535之间的,但是我们在使用的时候最好使用1024之后的,因为之前都是被占用的公认端口,参数0表示可以在任何空闲端口上创建套接字;accept方法侦听并接受到此套接字的连接,返回一个新套接字(客户端套接字),该方法在接收到传入连接之前会一直阻塞getInputStream()方法返回套接字的输入流,还有对应的getOutputStream()方法,返回套接字的输出流。

/*
 * TCP客户端
 */
public class TcpClient {
	public static void main(String[] args) throws UnknownHostException, IOException {
//		1. 创建Socket,指定服务器IP + 端口号。
		Socket socket=new Socket("172.16.0.133", 6666);
//		2. 使用输出流,发送请求数据到服务器。
		OutputStream os=socket.getOutputStream();
        //使用转换流,写入中文
		BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(os, "utf-8"));
		bw.write("我喜欢你");
//		3. 使用输入流,读取服务器回复的数据。【可选】
//		4. 释放资源。
		bw.close();
		socket.close();
	}
}

第一步中,端口号要和服务器套接字的端口一致,客户端套接字一旦创建之后,就和服务器建立了连接。

启动程序时,应该先运行服务器端代码,结果如下(程序处于阻塞状态):

服务器已启动...

再运行客户端代码,结果如下:

服务器已启动...
客户发送:我喜欢你

4.2.3 案例一分析

首先创建服务器套接字ServerSocket并绑定端口,然后创建客户端套接字Socket与服务器建立连接;

服务器套接字ServerSocket调用accept()侦听连接请求,然后返回一个套接字接收字节流;

客户端套接字可以通过流发送信息给服务器的Socket套接字,服务器的Socket套接字也可以通过流响应客户端套接字。

4.2.4 TCP编程案例二

  • TCP编程实现客户端上传文件给服务器端。
/**
 * TCP服务端
 */
public class TcpFileServer {
	public static void main(String[] args) throws Exception{
		//1. 创建ServerSocket,绑定端口
		ServerSocket listener=new ServerSocket(9999);
		//2. 侦听并接受到此套接字的连接
		System.out.println("服务器已启动...");
		Socket socket=listener.accept();
		//3. 获取输入流
		InputStream is=socket.getInputStream();
		//4. 边读取,边保存
		FileOutputStream fos=new FileOutputStream("d:\\MrG2.jpg");
		//创建缓冲区
		byte[] buf=new byte[1024*4];
		int count=0;
		while((count=is.read(buf))!=-1) {
			fos.write(buf,0,count);
		}
        System.out.println("服务器接收完毕。");
		//关闭资源
		fos.close();
		is.close();
		socket.close();
		listener.close();
	}
}

步骤和案例一差不多,只是换成了文件的读写。服务器端从输入流中读取数据到缓冲区,又从缓冲区写入到输出流到d:\\MrG2.jpg中,在这个案例中,我传递的是一张图片。

/**
 * TCP客户端
 */
public class TcpFileClient {
	public static void main(String[] args) throws Exception{
		//1. 创建Socket,并制定IP地址和端口
		Socket socket=new Socket("172.16.0.133",9999);
		//2. 使用输出流
		OutputStream os=socket.getOutputStream();
		//3. 读取文件
		FileInputStream fis=new FileInputStream("d:\\MrG1.jpg");
		//创建缓冲区
		byte[] buf=new byte[1024*4];
		int count=0;
		//边读取,变写入流
		while((count=fis.read(buf))!=-1) {
			os.write(buf,0,count);
		}
		System.out.println("文件已发送。");
		//4. 关闭资源
		fis.close();
		os.close();
		socket.close();
	}
}

客户端从本地读取文件数据到流中,然后将数据写入到输出流中。

首先启动服务器(程序堵塞中):

服务器已启动...

然后启动客户端,客户端控制台显示如下:

文件已发送。

服务器端控制台显示如下:

服务器已启动...
服务器接收完毕。

打开D盘下的文件发现多了一个MrG2.jpg,说明程序运行成功。

4.2.5 TCP编程案例三

  • TCP实现多个客户端发送数据给服务器端。

服务器端要接收多个客户端发送的数据,服务器端就需要创建多个线程,使每个线程独立处理每个客户端的任务。

//TCP服务器端
public class TcpChatServer {
	public static void main(String[] args) throws Exception{
		//创建服务器套接字
		ServerSocket listener=new ServerSocket(10086);
		System.out.println("服务器已连接...");
		//接收客户端请求
		while(true) {
			//死循环,来一个客户端请求接收一个
			Socket socket=listener.accept();
			System.out.println(socket.getInetAddress()+"进来了。");
			//创建子线程来处理客户端发送的数据
			new SocketThreads(socket).start();
		}	
	}
}
//线程类
public class SocketThreads extends Thread{
	private Socket socket;
	
	public SocketThreads(Socket socket) {
		this.socket=socket;
	}
	@Override
	public void run() {	
		if(socket!=null) {
			BufferedReader br=null;
			try {
				//创建输入流
				InputStream is=socket.getInputStream();
				//使用转换流以接收中文
				br=new BufferedReader(new InputStreamReader(is,"utf-8"));
				while(true) {
					String data=br.readLine();
					//客户端已关闭
					if (data==null) {
						break;
					}
					if (data.equals("quit")||data.equals("退出")) {
						break;
					}
					System.out.println(socket.getInetAddress()+"说:"+data);
				}		
			} catch (IOException e) {			
				e.printStackTrace();
			}finally {
				try {
					br.close();
					socket.close();
					System.out.println(socket.getInetAddress()+"退出了。");
				} catch (IOException e) {
					e.printStackTrace();
				}			
			}
		}	
	}
}
//TCP客户端
public class TcpChatClient {
	public static void main(String[] args) throws Exception {
		//创建客户端套接字
		Socket socket=new Socket("172.16.0.133",10086);
		System.out.println("客户端已建立连接...");
		//获取输出流
		OutputStream os=socket.getOutputStream();
		BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(os,"utf-8"));
		while(true) {
			Scanner in=new Scanner(System.in);
			String data=in.nextLine();
			bw.write(data);
			//输入换行符,否则线程类中readLine()不会返回
			bw.newLine();
			bw.flush();
			if (data.equals("quit")||data.equals("退出")) {
				break;
			}
		}
		bw.close();
		socket.close();
	}
}

然后再新建一个客户端类,代码同上,修改一下IP地址(当然IP地址必须是存在的)。

  1. 首先启动服务器查看服务器端代码的控制台:
服务器已连接...
  1. 然后启动客户端程序1,客户端1代码控制台:
客户端已建立连接...

服务器代码控制台:

服务器已连接...
/172.16.0.133进来了。
  1. 在客户端代码的控制台中输入信息后回车,服务器端代码控制台:
服务器已连接...
/172.16.0.133进来了。
/172.16.0.133说:有人吗?
  1. 启动客户端程序2,服务器代码控制台显示:
服务器已连接...
/172.16.0.133进来了。
/172.16.0.133说:有人吗?
/10.2.51.23进来了。
  1. 在客户端2控制台输入信息后回车,服务器代码控制台显示:
服务器已连接...
/172.16.0.133进来了。
/172.16.0.133说:有人吗?
/10.2.51.23进来了。
/10.2.51.23说:有,你是GG还是MM?
  1. 然后客户端程序输出"quit"或者“退出”,服务器控制台显示:
/172.16.0.133进来了。
/172.16.0.133说:有人吗?
/10.2.51.23进来了。
/10.2.51.23说:有,你是GG还是MM?
/172.16.0.133退出了。
/10.2.51.23退出了。

此处模拟了两个客户端,注意客户端中的IP地址必须是可达的。

4.2.6 TCP变成案例四

使用Socket实现注册登录

  • 使用Socket变成实现服务器端注册:

    • 注册信息保存在properties中。

    • 封装格式:

      id = {id : “1001” , name : “tang” , pwd : “036” , age = 21}

      注册成功后返回字符串“注册成功”。

      在下面的案例中,用户属性文件会以这样的键值对进行保存和加载。

  • 使用Socket变成实现服务器端登录:

    • 获取properties文件中的用户信息,进行用户id与密码的校验。
    • 校验成功后返回字符串“登录成功”。

首先演示实现服务器注册的功能。

//服务器端
public class UserServer {
	public static void main(String[] args) {
		new RegistThread().start();
	}
}

服务器端通过调用注册线程来执行程序:

//注册线程
public class RegistThread extends Thread{
	@Override
	public void run() {
		try {
			//创建服务器套接字
			ServerSocket listener=new ServerSocket(6666);
			//调用accept()
            System.out.println("注册服务器已启动...");
			Socket socket=listener.accept();
			//获取输入输出流
			//输入流用来读取客户端发送的数据
			BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream(),"utf-8"));
			//输出流用来响应客户端
			BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"utf-8"));
			//接收客户端发送的数据
			//{id : 1001 , name : tang , pwd : 036 , age = 21}
			String json=br.readLine();
			//全掉前后的大括号,然后按逗号分隔成字符串数组
			//id : 1001 , name : tang , pwd : 036 , age = 21
			String[] infos=json.substring(0, json.length()-1).split(",");
			//取第一个数组id:1001,然后用冒号分隔取1001
			String id=infos[0].split(":")[1];
			
			//加载用户属性文件
			Properties properties=Tools.loadProperties();
			//判断用户在属性文件中是否已存在
			if (properties.containsKey(id)) {
				bw.write("此用户已存在,请登录!");
			}else {
				//保存注册信息
				Tools.savaProperties(json);
				bw.write("注册成功!");
			}
			bw.newLine();
			bw.flush();
			bw.close();
			br.close();
			socket.close();
			listener.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

在注册线程中需要加载用户文件,用来判断注册的用户信息是否已经存在;如果不存在,还需要保存用户信息。这两个功能写进了Tools类文件中:

public class Tools {
	//1.加载属性文件
	public static Properties loadProperties() {
		//1.创建属性集合
		Properties properties=new Properties();
		//判断文件是否存在
		File file=new File("User.properties");
		if (file.exists()) {
			FileInputStream fis=null;
			try {
				//加载文件到流中
				fis=new FileInputStream(file);
				//从流中加载数据
				properties.load(fis);
			} catch (IOException e) {
				e.printStackTrace();
			}finally {
				try {
					fis.close();
				} catch (IOException e) {
					e.printStackTrace();
				}				
			}
		}
		return properties;
	}
	//2.保存用户信息
	public static void savaProperties(String json) {
		//取id
		String[] infos=json.substring(0, json.length()-1).split(",");
		String id=infos[0].split(":")[1];
		//创建Properties集合保存用户信息
		Properties properties=new Properties();
		properties.setProperty(id, json);
		FileOutputStream fos;
		BufferedWriter bw = null;
		try {
			//以追加的方式创建写入文件的流
			fos = new FileOutputStream("User.properties",true);
			bw=new BufferedWriter(new OutputStreamWriter(fos));
			//将集合中的数据保存到流中
			properties.store(bw, "");
		} catch (IOException e) {
			e.printStackTrace();
		}finally {
			if(bw != null) {
				try {
					bw.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

在客户端代码中,需要让用户选择注册或者登陆,然后分别调用注册和登陆功能:

public static void main(String[] args) {	
    System.out.println("请选择 1.注册 2.登录");
    Scanner in=new Scanner(System.in);
    int choice=in.nextInt();
    switch(choice) {
        case 1:
            try {
                //注册
                register();
            } catch (Exception e) {
                e.printStackTrace();
            }
            break;
        case 2:
            //演示注册,登陆暂未实现
            Login();
            break;
        default:
            break;
    }
}

在注册功能中实现客户端与服务端交换数据:

public static void register() throws Exception{
    //创建客户端套接字
    Socket socket=new Socket("172.16.0.133",6666);
    //获取输出流发送数据
    BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"utf-8"));
    bw.write(getUserInfo());
    bw.newLine();
    bw.flush();
    //获取输入流,读取服务器响应数据
    BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));		
    System.out.println("服务器回复:"+br.readLine());
    //关闭资源
    br.close();
    bw.close();
    socket.close();
}

客户端发送了用户输入的注册信息,获取用户输入的信息的功能写在了getUserInfo中:

//获取用户输入的注册信息,以特定格式返回。
public static String getUserInfo() {
    Scanner in=new Scanner(System.in);
    System.out.println("请输入用户ID(唯一):");
    String id=in.next();
    System.out.println("请输入姓名:");
    String name=in.next();
    System.out.println("请输入密码:");
    String pwd=in.next();
    System.out.println("请输入年龄:");
    int age=in.nextInt();
    String json="{id:"+id+",name:"+name+",pwd:"+pwd+",age:"+age+"}";
    return json;
}

服务器注册功能写完后,首先运行服务器端代码,然后运行客户端代码,按照提示输入用户信息,回车,显示注册成功:

请选择 1.注册 2.登录
1
请输入用户ID(唯一):
lazydog036
请输入姓名:
tang
请输入密码:
123
请输入年龄:
21
服务器回复:注册成功!

查看一下本地文件,多了一个User.properties文件,打开之后里面已经被写入了用户信息:

#
#Wed Nov 11 17:49:54 CST 2020
lazydog036={id\:lazydog036,name\:tang,pwd\:123,age\:21}

再次运行服务器和客户端程序,输入相同ID后回车,提示已存在:

请选择 1.注册 2.登录
1
请输入用户ID(唯一):
lazydog036
请输入姓名:
wang
请输入密码:
123
请输入年龄:
22
服务器回复:此用户已存在,请登录!

演示服务器登录,和注册大致差不多:

//登录线程
public class LoginThread extends Thread{
	@Override
	public void run() {
		try {
			//创建服务器套接字
			ServerSocket listener=new ServerSocket(7777);
			//调用accept()
			System.out.println("登录服务器已启动...");
			Socket socket=listener.accept();
			//获取输入输出流
			//输入流用来读取客户端发送的数据
			BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream(),"utf-8"));
			//输出流用来响应客户端
			BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"utf-8"));
			//接收客户端发送的数据
			//{id : 1001 , pwd : 036 }
			String json=br.readLine();
			//全掉前后的大括号,然后按逗号分隔成字符串数组
			//id : 1001 , pwd : 036 
			String[] infos=json.substring(0, json.length()-1).split(",");
			//取第一个数组id:1001,然后用冒号分隔取1001
			String id=infos[0].split(":")[1];
			
			//加载用户属性文件
			Properties properties=Tools.loadProperties();
			//判断用户在属性文件中是否已存在
			if (properties.containsKey(id)) {
				//获取用户输入的密码
				String pwd=json.substring(0, json.length()-1).split(",")[1].split(":")[1];
				//获取文件中存储的密码
				String pwd2=properties.getProperty(id).split(",")[2].split(":")[1];
				if (pwd.equals(pwd2)) {
					bw.write("登录成功!");
				}else {
					bw.write("密码错误,请重试!");
				}			
			}else {				
				bw.write("此用户不存在,请先注册!");
			}
			bw.newLine();
			bw.flush();
			bw.close();
			br.close();
			socket.close();
			listener.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

登录线程中的代码和注册线程差不多,只是在验证用户信息的时候改成判断密码是否正确。需要注意的是登录线程需要使用其他端口,不能占用服务器注册线程的端口;在接收数据的时候只接收用户名和密码。

//登录功能
public static void Login() throws Exception{
    //创建客户端套接字
    Socket socket=new Socket("172.16.0.133",7777);
    //获取输出流发送数据
    BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"utf-8"));
    bw.write(getLoginInfo());
    bw.newLine();
    bw.flush();
    //获取输入流,读取服务器响应数据
    BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));		
    System.out.println("服务器回复:"+br.readLine());
    //关闭资源
    br.close();
    bw.close();
    socket.close();
}

和注册功能几乎一样,但需要注意端口必须和服务器登录线程端口一致。在向服务器发送数据的时候调用了getLoginInfo方法获取用户输入的用户ID和密码:

public static String getLoginInfo() {
    Scanner in=new Scanner(System.in);
    System.out.println("请输入用户ID(唯一):");
    String id=in.next();
    System.out.println("请输入密码:");
    String pwd=in.next();
    String json="{id:"+id+",pwd:"+pwd+"}";
    return json;
}

然后在服务器代码中启动两个线程并运行:

public class UserServer {
	public static void main(String[] args) {		
		new RegistThread().start();
		new LoginThread().start();
	}
}
登录服务器已启动...
注册服务器已启动...

启动客户端选择登录功能,输入已存在的账号:

请选择 1.注册 2.登录
2
请输入用户ID(唯一):
lazydog036
请输入密码:
123
服务器回复:登录成功!

再次启动服务器和客户端程序,输入错误的账号:

请选择 1.注册 2.登录
2
请输入用户ID(唯一):
lazydog036
请输入密码:
666
服务器回复:密码错误,请重试!

到这里案例四就结束了~

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值