JavaEE-网络编程

网络编程

网络模型概述

计算机网络:

计算机网络是指将地理位置不同计算机及其外部部件,通过通信线路连接起来,在网络编程协议下,实现不同计算机之间的信息共享以及信息交流的计算机系统。

网络编程的目的:

传播交流信息(无线电台):数据交换,通信。

想要达到这些效果需要做什么:

  1. 找到对方IP地址;
  2. 数据要发送到对方指定的应用程序上。
    为了标识这些应用程序,所以给这些网络应用程序都用数字进行标识。
    为了方便称呼这些数字,我们把这些数字叫做“端口”,这些端口不是物理上的端口,而是逻辑端口。
    我们的网络应用程序一般可以对应一到多个数字标识(就是逻辑端口)
  3. 定义通信规则,这个通信规则称之为协议。

两个主机想进行通信,就必须使用相同的通信协议。

  • IP地址一共分为A、B、C、D共4段(IPv4),每一段用8个位表示,最大值为255。有一个IP地址较为特殊——127.0.0.1,如果没有配置任何IP地址,这就是本机默认的IP地址。(该地址可以用于Ping测试网卡)另外,有一些地址保留作为局域网的地址使用。IPV6的IP地址不仅仅包含数字,还包含字母,数量很多足够使用!
  • 我们在用数字表示端口的时候,可以用0-65535(2^16),0-1024的端口一般被系统的程序所保留。常见端口:web:80;Tomcat服务器:8080;MySQL数据库:3306,这些默认的端口都可以改。凡是网络应用程序,都会有数字来标识端口,这样他们的数据才可以传输。
  • 常用的传输协议有UDP与TCP。

javaweb:网页编程 B/S

网络编程:TCP/IP C/S

网络要素

网络通信的模型

开放系统互联参考模型(OSI,Open System Interconnection Reference Model),它包括七层的协议体系,从下到上分别是——物理层、数据链路层、网络层、运输层、会话层、表示层、应用层。
  传输控制协议/网际协议(TCP/IP,Transmission Control Protocol/Internet Protocol),它从下到上包括网络接口层、网际层IP、运输层(TCP/UDP)、应用层。
  我们在学习计算机网络的时候,通常采用折中的方法,学习5层协议的体系结构——从下到上分别是:物理层、数据链路层、网络层、运输层、应用层。

TCP/IP参考模型:

image-20221202153729680

网络编程针对传输层 TCP,UDP

学习网络编程,其实就是在TCP/IP模型中的网络层和运输层(传输层)进行操作。

小结:

  1. 网络编程两个主要问题
  • 如何准确定位到网络上的一台或多台主机
  • 找到主机之后如何进行通信
  1. 网络编程中的要素
  • IP和端口号
  • 网络通信协议:TCP,UDP
  1. 万物皆对象

IP地址

ip地址:InterAddress

—网络中设备的标识

—不易记忆,可用主机名

—本地回环地址:127.0.0.1,主机名:localhost

  • 唯一定位一台网络上的计算机

  • 127.0.0.1:本机,localhost(C:\Windows\System32\drivers\etc\hosts 可以更改名称)

  • ip地址分类

    • IPv4 / IPv6

      • ==IPV4:==127.0.0.1,四个字节组成。0 ~ 255, 42亿~;30亿都在北美,亚洲4亿。2001年就用尽了;
      • ==IPV6:==fe80::755f:fc6c:2ebc:b6e6%18,128位,16个字节组成。8个无符号整数!可以给地球上每粒沙子分配;
        • 128位 指二进制 位数是 128。
          1个字节 是 8 位 2进制。
          所以 等于 128/8 = 16 个字节。
      //例如:		2001:0bb2:aaaa:0015:0000:0000:1aaa:1312
      
    • 公网(互联网) / 私网(局域网)

      • ABCD类地址

      • 192.168.xx.xx,专门给组织内部使用

在这里插入图片描述

  • 域名:记忆IP问题
    • IP:www.vip.com

练习:InetAddress类的常用方法

//获取网站ip地址
InetAddress address = InetAddress.getByName("www.baidu.com");
System.out.println(address);

//常用方法
System.out.println(address.getHostAddress()); // ip地址(常用)
System.out.println(address.getAddress()); //返回原生地址,没什么用
System.out.println(address.getCanonicalHostName()); //规范的名字
System.out.println(address.getHostName()); //域名(或者本机电脑的名字)

端口

端口表示计算机上的一个程序的进程;

  • 不同的进程有不同的端口号!用来区分软件!
  • 被规定0 ~ 65535(电脑最多跑的进程数)
  • TCP,UDP:65535 * 2 ,单个协议下,端口号不能冲突(tcp:80 udp:80可以)

分类:

  • 公有端口 0~1023

    • HTTP:80
    • HTTPS:443
    • FTP:21
    • Telent:23
  • 程序注册端口:1024~49151,分配给用户或者程序

    • Tomcat:8080,MySQL:3306,Oracle:1521
    • 动态、私有:49152~65535
    netstat -ano #查看所有的端口
    netstat -ano | finstr "5900" #查看指定的端口
    tasklist | findstr "8696" #查看指定端口的进程
    ctrl + shift + ESC #打开任务管理器,死机也能用
    
    

练习:InetSocketAddress类

//创建套接字地址:IP地址和端口号
public class TestInetSocketAddress {
    public static void main(String[] args) {
        InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1",8080);
        System.out.println(socketAddress);

        System.out.println(socketAddress.getAddress()); //地址
        System.out.println(socketAddress.getHostName());
        System.out.println(socketAddress.getPort()); //端口号
    }
}

传输协议:TCP和UDP

协议:约定,就好比我们现在说的是普通话。

**网络通信协议:**速率,传输码率,传输控制……

**问题:**非常的复杂?

大事化小:分层!

image-20230112165929087

TCP/IP协议簇:

重要的两个协议:

TCP:用户传输协议
UDP:用户数据报协议

出名的协议:TCP,IP(网络互连协议),实际上是一组协议

TCP,UDP对比:

TCP:打电话

  • 连接:稳定
  • 三次握手 四次挥手
  • 客户端,服务端
  • 传输完成,释放连接、效率低

UDP:发短信

  • 连接:不稳定
  • 客户端,服务端没有明显的界限
  • 不管有没有准备好,都可以发给你(导弹)
  • DDOS:洪水攻击!饱和攻击

扩展:三次握手,四次挥手

最少需要三次,保证稳定连接!

A:你瞅啥? #第一次握手:客户端向服务端申请连接
B:瞅你咋地?#第二次握手:服务端向客户端返回确认同意连接
A:干一场! #第三次握手:客户端发送确认报文段,完成连接

A:我要走了
B:你真的要走了吗?
B:你真的真的要走了吗?
A:我真的要走了

域名解析

image-20230112225911100

ip对象-InetAddress

java中操作IP地址的类——类 InetAddress

InetAddress:此类表示互联网协议 (IP) 地址

IP 地址是 IP 使用的 32 位或 128 位无符号数字,它是一种低级协议,UDP 和 TCP 协议都是在它的基础上构建的。
InetAddress 的实例包含 IP 地址,还可能包含相应的主机名(取决于它是否用主机名构造或者是否已执行反向主机名解析)。

InetAddress没有构造方法,它必然含有返回它本类对象的静态方法

package pack;
import java.net.*;

public class IPDemo
{	
	public static void main(String[] args) throws Exception
	{
		test_3();		
	}
	
	public static void test_1()throws Exception
	{
		//static InetAddress getLocalHost() 返回本地主机。 
		//首先我们使用getLocalHost()方法返回InetAddress的对象。这个方法会抛出异常UnknownHostException
		InetAddress iad = InetAddress.getLocalHost();
				
		// String toString() 将此IP地址转换为String。 
		System.out.println(iad.toString());
		/* 
		* DESKTOP-O5MEGBI/192.168.1.195:显示本机IP地址的主机名以及具体的IP地址字符串表示
		*/
		//可以使用下面2个方法获取IP地址的字符串表现形式以及相对应的主机名
		System.out.println("Address:"+iad.getHostAddress());//Address:192.168.1.195
		System.out.println("Name:"+iad.getHostName());//Name:DESKTOP-O5MEGBI
		
		//注意,我们所获取的地址都是本机解析获得的地址,如果是网络地址,我们不一定可以拿到网络上其他主机的名称
	}
	
	public static void test_2()throws Exception
	{
		//如果我们想获取任意一台主机的IP地址对象
		//static InetAddress getByName(String host) 在给定主机名的情况下确定主机的IP地址。 
		//主机名可以是机器名(如 "java.sun.com"),也可以是其 IP 地址的文本表示形式。如果提供字面值 IP 地址,则仅检查地址格式的有效性。
		
		InetAddress iad = InetAddress.getByName("192.168.1.195");
		System.out.println(iad.toString());// /192.168.1.195:主机地址没有解析出来
		System.out.println("Address:"+iad.getHostAddress());//Address:192.168.1.195
		System.out.println("Name:"+iad.getHostName());//Name:192.168.1.195
		//这里使用IP地址的文本形式寻找相应的主机名。如果IP地址与对应的主机名的映射关系没有在网络上,
		//我们的主机那IP地址去网络上寻找主机名,但是没有解析成功,那么返回的名字还是IP地址
		//也就是说,IP地址可以直接获得,但是主机名称还需要解析,有的时候可能解析不出来。
		   
		//如果我们使用主机地址来查询,就可以查询出来——经过试验,查询链接到宿舍wifi的其他主机也可以查询到!
		InetAddress ia = InetAddress.getByName("WINDOWS-LTOJAMN");
		System.out.println(ia.toString());// DESKTOP-O5MEGBI/192.168.1.195
		System.out.println("Address:"+ia.getHostAddress());//Address:192.168.1.195
		System.out.println("Name:"+ia.getHostName());//Name:DESKTOP-O5MEGBI
	}
	
	public static void test_3()throws Exception
	{
		//只要一个主机连上网络,我们就可以获取其主机地址,比如百度腾讯的地址我们都可以获取
		//但是百度主机可能有很多台,而且一个IP地址可能通过映射,对应多个服务器,这个时候返回的IP对象不唯一
		//static InetAddress[] getAllByName(String host) 在给定主机名的情况下,根据系统上配置的名称服务返回其IP地址所组成的数组。 
		InetAddress[] iads = InetAddress.getAllByName("www.baidu.com");
		for(InetAddress iad : iads)
		{
			System.out.println("Address:"+iad.getHostAddress());
			System.out.println("Name:"+iad.getHostName());
		}
		/*发现百度果然不止一台主机
		 Address:14.215.177.39
		Name:www.baidu.com
		Address:14.215.177.38
		Name:www.baidu.com
		 */
	}
}

Socket套接字

[Socket 套接字]{https://blog.csdn.net/weixin_44121529/article/details/124271711?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167860279916800192297151%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=167860279916800192297151&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-2-124271711-null-null.142v73insert_down2,201v4add_ask,239v2insert_chatgpt&utm_term=socket%E5%A5%97%E6%8E%A5%E5%AD%97&spm=1018.2226.3001.4187}

在说传输协议之前,我们先来介绍一个概念Scoket

Socket:连接应用层和传输层之间的套件,因此,每一个传输层连接有两个端点。
那么,传输层连接的端点是什么呢?不是主机,不是主机的IP地址,不是应用进程,也不是传输层的协议端口。传输层连接的端点叫做套接字(socket)

**套接字(socket)**是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。
套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

我们平时说的网络编程,其实就是socket的编程。简单地说,socket有以下4个特点:

  • Socket就是为网络服务提供的一种机制。
  • 通信的两端都有Socket,才能完成。
  • 网络通信其实就是Socket间的通信。
  • 数据在两个Socket间通过IO传输。

IDEA如何打开两个控制台窗口

image-20230213202550308

image-20230213202643168

image-20230213202723148

搞定

UDP协议

UDP相关介绍如下图:

image-20230114154054585

**DatagramSocket类与DatagramPacket**类描述如下:

public class DatagramSocket extends Object
此类表示用来发送和接收数据报包的套接字。——既用于封装发送接收功能的类。 

public final class DatagramPacket extends Object
此类表示数据报包。——既用于封装数据包的类。

代码实例如下:这里需要注意的是,发送端与接收端都是独立的运行程序,因此2个程序都有自己的主函数。事实上,在真正的运行环境下,这2个程序应该运行于不同的主机。
  首先,我们按照视频中的代码与方法来操作(注意,用上一节的方法查询,我的主机的IP地址是:192.168.0.106,而原代码中是192.168.1.254,这里要特别注意,因为一个小错误浪费很久!)

发送端

无论是先启动发送端还是启动接收端,都可以接收到信息。(记住我的ip地址是:192.168.0.106)

package net.udp;

/*
需求:通过udp传输方式,将一段文字数据发送出去。,
定义一个udp发送端。
思路:
1,建立updsocket服务。
2,提供数据,并将数据封装到数据包中。
3,通过socket服务的发送功能,将数据包发出去。
4,关闭资源。

*/


import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

class  UDPSendDemo
{
    public static void main(String[] args) throws Exception
    {
        System.out.println("发送端启动。。。。");
        //1,创建udp服务。通过DatagramSocket对象。
        DatagramSocket ds = new DatagramSocket();

        //2,确定数据,并封装成数据包。DatagramPacket(byte[] buf, int length, InetAddress address, int port)

        byte[] buf = "udp传输,哥们来了".getBytes();
        DatagramPacket dp =
                new DatagramPacket(buf,buf.length, InetAddress.getByName("192.168.0.106"),10000);

        //3,通过socket服务,将已有的数据包发送出去。通过send方法。
        ds.send(dp);

        //4,关闭资源。

        ds.close();

    }
}

接收端

package net.udp;


import java.net.DatagramPacket;
import java.net.DatagramSocket;

/*
需求:定义一个应用程序,用于接收udp协议传输的数据并处理的,既定义一个UDP的接收端。
思路:
1、定义udpsocket服务。因为是要接收数据,必须要明确一个端口号。,其实就是给这个接收数据的网络应用程序定义一个数字标识。(不定义的话系统会分配一个随机的端口)
	方便于明确哪些数据过来该应用程序可以处理。
2、定义一个数据包,因为要存储接收到的字节数据,数据包对象中有更多功能可以提取字节数据中的不同数据信息。
3、通过socket服务的receive方法将收到的数据存入已定义好的数据包中。
4、通过数据包对象的特有功能,将这些不同的数据取出,打印在控制台上。
5、关闭资源。
*/
public class UDPReceDemo//这里差点犯错,注意类表示的是对象,是无法抛出异常的,只有方法才能抛出异常,并且最后都得在方法中处理
{
    public static void main(String[] args) throws Exception
    {
        //首先,定义UDPSocket服务,并将接收端用端口10000表示
        DatagramSocket ds = new DatagramSocket(10000);

    /*
     其次,定义数据包接收,我们为了可以持续接收发送端发送过来的数据,我们要将接收端一直开着,否则我们接收端接收一次之后,程序就会停止,
     下一次发送端继续发送,接收端无法收到。因此,我们这里设计一个while循环,这里并不会发生死循环,因为receive是阻塞式方法,
    接收到数据它就会执行,没有接受数据就会停止进入休眠状态,因此不会有死循环的现象。
    我们修改发送端发送的数据,发现持续循环的接收端可以接收到这些数据!!!

    注意:不能将socket服务的定义也放到while中,这种循环建立服务的行为会出现bindException异常(见视频23-09,2.00解析)
    */
        while(true)
        {
            byte[] buf = new byte[1024];//定义一个字节数组用于接收
            //DatagramPacket(byte[] buf, int length) 构造 DatagramPacket,用来接收长度为 length 的数据包。
            DatagramPacket dp = new DatagramPacket(buf , buf.length);

            //使用DatagramSocket对象接收数据包对象,接收的数据包对象保存在本类的dp中
            ds.receive(dp);//注意,receive是一个阻塞式方法,如果没有接收到数据它就会一直等待

            //使用DatagramPacket对象的方法取出各类数据——这个时候接收的数据包对象已经保存到dp中
            //InetAddress getAddress() 返回某台机器的 IP地址,此数据报将要发往该机器或者是从该机器接收到的。
            String IP = dp.getAddress().getHostAddress();//这里是返回发送数据包的机器的主机IP地址

            //byte[] getData() 返回数据缓冲区。 ——将接收到的数据的字节数组转换为String
            String data = new String(dp.getData() , 0 , dp.getLength());

            // int getPort() 返回某台远程主机的端口号,此数据报将要发往该主机或者是从该主机接收到的。
            int port = dp.getPort();

            //打印接收到的数据
            System.out.println("发送机器:"+IP+"\r\n"+"接收到的数据:"+data+"\r\n"+"发送机器的端口"+port);
        }
        //关闭socket服务
//		ds.close();
    }
}


image-20230114155234884

通过上面的例子发现,每一次发送的都是同一句话,就算修改发送内容,还是要重新编译才能运行。我们通过键盘录入的方式来写入是最方便的!相应的代码如下

首先是UdpSend发送端
-------------
package pack;
import java.net.*;
import java.io.*;

public class  UdpSend
{
	public static void main(String[] args) throws Exception
	{
		DatagramSocket ds = new DatagramSocket();//同样定义发送端的socket服务,发送端不指定固定端口
		//为了实现键盘录入的功能,我们这里使用缓冲区读取
		BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
		
		String line = null;
		//我们这里使用while循环来表示持续发送
		while((line=bufr.readLine()) != null)
		{
			if(line.equals("over"))
				break;//当输入“over”就要结束循环
			byte[] buf = line.getBytes();//获取要发送的数据的字节数组
			//将各种信息封装为数据包对象
			DatagramPacket dp = 
					new DatagramPacket(buf , buf.length , InetAddress.getByName("192.168.1.195"), 10006);
			ds.send(dp);
		}
		ds.close();
	}
}
-------------------

其次是接收端
-----------------------------
package pack;

import java.net.*;
public class UdpRece
{
	public static void main(String[] args) throws Exception
	{
		DatagramSocket ds = new DatagramSocket(10006);//同样定义接收端的socket服务
		
		//使用while循环持续接收
		while(true)
		{
			byte[] buf = new byte[1024];
			DatagramPacket dp = 
					new DatagramPacket(buf , buf.length);
			ds.receive(dp);//将接收到的数据封装到DatagramPacket对象方便取出各类数据
			
			String ip = dp.getAddress().getHostAddress();			
			String data = new String(dp.getData() , 0 , dp.getLength());			
			int port = dp.getPort();
			System.out.println("发送机器:"+ip+"\r\n"+"接收到的数据:"+data+"\r\n"+"发送机器的端口"+port);
		}
	}
}

聊天程序

如果想实现群里,我们局域网IP地址中有一个广播地址,如“192.168.1.255”就是我们这一段局域网IP的广播地址。如果我们想发送数据给局域网中的所有成员,就可以发给这个IP地址广播出去。
  
  。

我们现在想实现聊天的功能,那么一个端口需要同时有发送与接收的功能。不同于上面,其实有2个进程,一个在实现发送一个在实现接收。我们使用一个进程,里面有发送与接收2个线程,来实现类似于聊天的功能。具体如下代码
  需要注意的是,与之前不同,之前是有2个进程,一个进程发送一个进程接收,他们的发送与接收端是不一样的,因此我们可以在一个端口发送,在另一个端口接收。而在这里,如果我们要模拟实现聊天的功能,那么一个进程里面有发送的线程也有接收的线程,那么进行聊天的两个进程的发送端可以不指定固定端口,但是接收端的端口一定要不同!
  既ChatDemo中数据发送到本机的10008端口,这个进程自身用10007端口接收,而ChatDemo2中发送到本机的10007端口,这个进程自身用10008端口接收,这样才能模拟2台主机聊天(既用2个端口模拟2台主机)

相应的代码如下:

需求分析:
/*
编写一个聊天程序。
有收数据的部分,和发数据的部分,这两部分需要同时执行,那就需要用到多线程技术,一个线程控制收,一个线程控制发。
因为收和发动作是不一致的,所以要定义两个run方法,而且这两个方法要封装到不同的类中。
*/
------------------------
首先是Test项目下的ChatDemo----
package pack;
import java.net.*;
import java.io.*;
//我们现在只写一个端口的发送与接收的代码,用2个线程表示
//因为发送端与接收端2个线程的代码不同,必须分2个类搞2个run()方法

//首先是发送的类
class Send implements Runnable
{
	//发送的时候不知道会传入什么样的DatagramSocket对象,我们这里再构造方法将其插入
	private DatagramSocket ds;
	Send(DatagramSocket ds)
	{
		this.ds = ds;
	}
	
	//复写run()方法
	public void run() 
	{
		BufferedReader bufr = null;
		
		try
		{
			bufr = new BufferedReader(new InputStreamReader(System.in));
			String line = null;
			//我们这里使用while循环来表示持续发送
			while((line=bufr.readLine()) != null)
			{
				byte[] buf = line.getBytes();//获取要发送的数据的字节数组
				//将各种信息封装为数据包对象
				DatagramPacket dp = 
						new DatagramPacket(buf , buf.length , InetAddress.getByName("192.168.1.195"), 10008);
				//发送到本机的10008端口,这个进程自身用10007端口接收
				ds.send(dp);
				if(line.equals("886"))
					break;//当输入“over”就要结束循环
			}
			ds.close();
		}
		catch(Exception e)
		{
			throw new RuntimeException("发送端失败");
		}//为了方便这里就不写关闭缓冲区代码	
	}
	
}

//下面是接收的类
class Rece implements Runnable
{
	private DatagramSocket ds;
	Rece(DatagramSocket ds)
	{
		this.ds = ds;
	}
	
	//同样复写run()方法
	public void run() 
	{
		try
		{		
			//使用while循环持续接收
			while(true)
			{
				byte[] buf = new byte[1024];
				DatagramPacket dp = 
						new DatagramPacket(buf , buf.length);
				ds.receive(dp);//将接收到的数据封装到DatagramPacket对象方便取出各类数据
				
				String ip = dp.getAddress().getHostAddress();			
				String data = new String(dp.getData() , 0 , dp.getLength());			
				
				if("886".equals(data))
				{
					System.out.println(ip+"....离开聊天室");
					break;
				}
				System.out.println(ip+":"+data);
			}
		}
		catch (Exception e)
		{
			throw new RuntimeException("接收端失败");
		}
		
	}
	
}

public class ChatDemo 
{

	public static void main(String[] args) throws Exception
	{
		DatagramSocket sendSocket = new DatagramSocket();
		DatagramSocket receSocket = new DatagramSocket(10007);
		//在main中开启个线程
		new Thread(new Send(sendSocket)).start();//发送线程,不固定发送端端口
		new Thread(new Rece(receSocket)).start();//指定接收端口为10007
	}
}

--------------------------
其次是Test项目下的ChatDemo2-----

import java.net.*;
import java.io.*;
//我们现在只写一个端口的发送与接收的代码,用2个线程表示
//因为发送端与接收端2个线程的代码不同,必须分2个类搞2个run()方法

//首先是发送的类
class Send implements Runnable
{
	//发送的时候不知道会传入什么样的DatagramSocket对象,我们这里再构造方法将其插入
	private DatagramSocket ds;
	Send(DatagramSocket ds)
	{
		this.ds = ds;
	}
	
	//复写run()方法
	public void run() 
	{
		BufferedReader bufr = null;
		
		try
		{
			bufr = new BufferedReader(new InputStreamReader(System.in));
			String line = null;
			//我们这里使用while循环来表示持续发送
			while((line=bufr.readLine()) != null)
			{
				byte[] buf = line.getBytes();//获取要发送的数据的字节数组
				//将各种信息封装为数据包对象
				DatagramPacket dp = 
						new DatagramPacket(buf , buf.length , InetAddress.getByName("192.168.1.195"), 10007);
				//发送到本机的10007端口,这个进程自身用10008端口接收
				ds.send(dp);
				if(line.equals("886"))
					break;//当输入“over”就要结束循环
			}
			ds.close();
		}
		catch(Exception e)
		{
			throw new RuntimeException("发送端失败");
		}//为了方便这里就不写关闭缓冲区代码	
	}
	
}

//下面是接收的类
class Rece implements Runnable
{
	private DatagramSocket ds;
	Rece(DatagramSocket ds)
	{
		this.ds = ds;
	}
	
	//同样复写run()方法
	public void run() 
	{
		try
		{		
			//使用while循环持续接收
			while(true)
			{
				byte[] buf = new byte[1024];
				DatagramPacket dp = 
						new DatagramPacket(buf , buf.length);
				ds.receive(dp);//将接收到的数据封装到DatagramPacket对象方便取出各类数据
				
				String ip = dp.getAddress().getHostAddress();			
				String data = new String(dp.getData() , 0 , dp.getLength());			
				
				if("886".equals(data))
				{
					System.out.println(ip+"....离开聊天室");
					break;
				}
				System.out.println(ip+":"+data);
			}
		}
		catch (Exception e)
		{
			throw new RuntimeException("接收端失败");
		}
		
	}
	
}

public class ChatDemo2 
{

	public static void main(String[] args) throws Exception
	{
		DatagramSocket sendSocket = new DatagramSocket();
		DatagramSocket receSocket = new DatagramSocket(10008);
		//在main中开启个线程
		new Thread(new Send(sendSocket)).start();//发送线程,不固定发送端端口
		new Thread(new Rece(receSocket)).start();//指定接收端口为10008
	}
}

TCP协议

UDP分为发送端与接收端,而TCP分为客户端(Socket)与服务器端(ServerSocket)。

image-20230129160025427

相应的代码如下:(注意为了顺利连接,必须先启动服务器端!)

客户端与服务端

首先是客户端
----
package pack;
import java.net.*;
import java.io.*;
/*
客户端,
通过查阅socket对象,发现在该对象建立时,就可以去连接指定主机。
因为tcp是面向连接的,所以在建立socket服务时,就要有服务端存在,并连接成功。形成通路后,在该通道进行数据的传输。

需求:给服务端发送给一个文本数据。
步骤:
1、创建Socket服务,并指定要连接的主机和端口。
2、获取socket的输出流OutputStream,并调用输出流的write服务向服务器端发数据
3、关闭Socket服务
*/

public class TcpClient {

	public static void main(String[] args) throws Exception
	{
		//首先,建立Socket服务
		//Socket(String host, int port) 创建一个流套接字并将其连接到指定主机上的指定端口号。
		Socket sk = new Socket("192.168.0.106" , 20000);//发送到指定IP地址的20000端口
		//一当Socket服务建立成功,客户端与服务器端建立成功,就会有Socket流
		
		//获取输出流:OutputStream getOutputStream() 返回此套接字的输出流。 
		OutputStream os = sk.getOutputStream();
		byte[] buf = "dlackjcvc clmax adcnja".getBytes();
		os.write(buf);//向服务器端发送数据
		
		sk.close();//关闭Socket
	}

}

---------------
其次是服务器端
package pack;
import java.net.*;
import java.io.*;
/*
需求:定义端点接收数据并打印在控制台上。

服务端:
1、建立服务端的socket服务:ServerSocket(),并监听一个端口。
2、获取连接过来的客户端对象。通过ServerSokcet的 accept方法。没有连接就会等,所以这个方法阻塞式的。
3、客户端如果发过来数据,那么服务端要使用对应的客户端对象,并获取到该客户端对象的读取流来读取发过来的数据,并打印在控制台。
4、关闭服务端。(可选)
*/
public class TcpServer {

	public static void main(String[] args) throws Exception
	{
		//首先建立服务器端Socket服务
		ServerSocket ss = new ServerSocket(20000);//监听20000端口,因为客户端发送到20000端口

		while(true)//通过一个循环来持续接收获取数据
		{
			//获取连接过来的客户端的socket服务
			//Socket accept()侦听并接受到此套接字的连接。 
			Socket sk = ss.accept();
			
			//获取客户端发送过来的数据,那么要使用客户端对象的读取流来读取数据。
			//InetAddress getInetAddress() 返回套接字连接的地址。 
			String ip = sk.getInetAddress().getHostAddress();//获取客户端的IP地址对象的本地IP地址
			
			//注意,服务器端没有流对象,必须获取客户端的流对象进行数据操作(视频23-11,13.00)
			//InputStream getInputStream() 返回此套接字的输入流。 通过客户端输入流来读取客户端发送的字节数组的数据
			InputStream is = sk.getInputStream();//获取客户端的读取流来读取其发送的数据
			byte[] buf = new byte[1024];
			int len = is.read(buf);//读取客户端通过字节流发送的数据
			String data = new String(buf,0,len);
			System.out.println(ip+"----"+data);
			
			sk.close();//上面创建了Socket服务,记得将其关闭
		}
//		ss.close();//关闭服务器端——可选
	}
}

客户端与服务器端的互访

/*
演示tcp的传输的客户端和服务端的互访。
需求:客户端给服务端发送数据,服务端收到后,给客户端反馈信息。
*/
----------
客户端
package pack;
import java.net.*;
import java.io.*;
/*
客户端:
1、建立socket服务。指定要连接主机和端口。
2、获取socket流中的输出流。将数据写到该流中。通过网络发送给服务端。
3、获取socket流中的输入流,将服务端反馈的数据获取到,并打印。
4、关闭客户端资源。

*/

public class TcpClient {

	public static void main(String[] args) throws Exception
	{
		Socket sk = new Socket("192.168.0.106", 20002);
		OutputStream os = sk.getOutputStream();
		byte[] outBuf = "服务器你好呀".getBytes();
		os.write(outBuf);
		
		//获取输入流接收服务器的反馈
		InputStream is = sk.getInputStream();
		byte[] inBuf = new byte[1024];
		int len = is.read(inBuf);
		System.out.println("服务器的反馈是:"+new String(inBuf,0,len));
		sk.close();
	}

}
-----------------------
服务器端
package pack;
import java.net.*;
import java.io.*;
/*
需求:定义端点接收数据并打印在控制台上。并反馈数据给客户端。

服务端:
1、建立服务端的socket服务:ServerSocket(),并监听一个端口。
2、获取连接过来的客户端对象。通过ServerSokcet的 accept方法。没有连接就会等,所以这个方法阻塞式的。
3、客户端如果发过来数据,那么服务端要使用对应的客户端对象,并获取到该客户端对象的读取流来读取发过来的数据,并打印在控制台。
4、将反馈数据发送给客户端
5、关闭服务端。(可选)
*/
public class TcpServer {

	public static void main(String[] args) throws Exception
	{
		ServerSocket ss = new ServerSocket(20002);
		Socket sk = ss.accept();//获取客户端的Socket对象,其实accept是阻塞式方法,没有接收到客户端的Socket对象就会一直等待。
		
		String ip = sk.getInetAddress().getHostAddress();//获取客户端IP地址
		InputStream is = sk.getInputStream();
		byte[] inBuf = new byte[1024];
		int len = is.read(inBuf);
		System.out.println("客户端"+ip+"数据:"+new String(inBuf,0,len));
		
		Thread.sleep(10000);//接收到数据后延时一下
		
		OutputStream os = sk.getOutputStream();
		os.write("你好客户端,我收到了".getBytes());
		
		sk.close();
		ss.close();
	}
}

练习

注意,局域网中IP地址在网络重连之后可能会改变,注意如果客户端与服务端无法连接,就测试一下是不是IP地址改变

文本转换
/*
需求:建立一个文本转换服务器。
客户端给服务端发送文本,服务端会将文本转成大写在返回给客户端。
而且客户度可以不断的进行文本转换。当客户端输入over时,转换结束。
*/
------------
首先是TcpClientpackage pack;
import java.net.*;
import java.io.*;

/*
 分析:
客户端:
既然是操作设备上的数据,那么就可以使用io技术,并按照io的操作规律来思考。
源:键盘录入。
目的:网络设备,网络输出流。
而且操作的是文本数据,可以选择字符流
——
1、将键盘键入的InputStream流转换为字符流缓冲区
2、将Socket类返回的OutputStream转换为字符流缓冲区BufferedWriter
3、将Socket类返回的InputStream转换为字符流缓冲区BufferedReader

步骤
1、建立服务。
2、获取键盘录入。
3、将数据发给服务端。
4、后去服务端返回的大写数据。
5、结束,关资源。
 */

public class TcpClient {

	public static void main(String[] args) throws Exception
	{
		Socket sk = new Socket("192.168.1.195",20003);
		
		//我们需要从键盘输入信息,因此还需要定义一个读取键盘的字符串缓冲区
		BufferedReader bufr = 
				new BufferedReader(new InputStreamReader(System.in));
		
		//由于客户端需要获取服务器端的反馈,我们还需要获取Socket的输入字节流并装换为字符流缓冲区
		BufferedReader bufIn = 
				new BufferedReader(new InputStreamReader(sk.getInputStream()));
		//获取Socket服务的输出字节流,并将其装换为字符流缓冲区,便于写出
//		BufferedWriter bufOut = 
//				new BufferedWriter(new OutputStreamWriter(sk.getOutputStream()));
//这里我们可以用PrintWriter写出流来简化客户端向服务器写出流的服务
//PrintWriter(OutputStream out, boolean autoFlush)通过现有的 OutputStream 创建新的 PrintWriter。
		PrintWriter pw = new PrintWriter(sk.getOutputStream() , true);
		
		
		
		//当键盘有内容的时候,持续读取并发送给服务器端,同时接手服务器端的反馈
		String line = null;
		while((line = bufr.readLine()) != null)
		{
			if(line.equals("over"))
				break;//如果控制台输入“over”,我们结束循环,不再向服务器端发送数据。
			//这里的判断放在客户端就可以,不需要在服务器端判断,因为客户端一当不发送,服务器端也不会接收到!
			
			/*
			//使用Socket的缓冲区写出流将其写出到服务器端
			bufOut.write(line);
//我们第一次运行发现客户端与服务端连接成功,但是数据没有发送成功(见视频23-13,18.30开始解析)
//因为readLine()方法只有在读到回车符的时候才会返回数据,这里我们只写出一行而没有加换行的回车符,这样TcpServer的readLine()方法
//会一直在等待回车符,因而它一直在读取状态,这样就不会执行循环体里面的内容!我们需要在这里再写出一个回车符!
//这和我们在键盘录入的时候需要回车TcpClient的readLine()才有反应一样。
			bufOut.newLine();//注意换行,这个是结束标记,一定要记得加上!
			bufOut.flush();//注意缓冲区的写出需要刷新!
			*/
			pw.println(line);//PrintWriter的println方法不仅可以写出一行数据,而且还会自动换行刷新缓冲区!
			
			
			//同时使用Socket的缓冲区读取流读取服务器端口的反馈并打印到控制台
			String newLine = bufIn.readLine();
			System.out.println("反馈信息为:"+newLine);
		}
		//为什么客户端结束而服务端也会跟着结束?明明服务端没有判断跳出循环?(视频23-13,25.30)这里很重要!!!——因为客户端关闭Socket流,底层会传递一个null给服务端的readLine()方法,这样子服务端的readLine也会跳出循环
		bufr.close();//关闭键盘读取缓冲区
		sk.close();//关闭Socket服务
	}
}
/*
该例子出现的问题。
现象:客户端和服务端都在莫名的等待,为什么呢?
因为客户端和服务端都有阻塞式方法,这些方法么没有读到结束标记,那么就一直等,而导致两端,都在等待。(见上面write()方法下面的解析)
(开发时再遇到这种现象,必须查询相应的阻塞式方法)
*/
---------------

其次是TCPServerpackage pack;
import java.net.*;
import java.io.*;

/*
服务端:
源:socket读取流。
目的:socket输出流。
都是文本,装饰。
*/
public class TcpServer {

	public static void main(String[] args) throws Exception
	{
		//首先创建服务器端的服务,并指定接收端口为20003
		ServerSocket ss = new ServerSocket(20003);
		//获取发送端的Socket对象
		Socket sk = ss.accept();
		
		//由于服务器端需要读取和写出客户端发送过来的数据,我们将Socket的输入以及输出流转换为字符串缓冲区
		//输出流
//		BufferedWriter bufOut = 
//				new BufferedWriter(new OutputStreamWriter(sk.getOutputStream()));
		PrintWriter pw = new PrintWriter(sk.getOutputStream() , true);//同样使用PrintWriter
		
		//输入流
		BufferedReader bufIn = 
				new BufferedReader(new InputStreamReader(sk.getInputStream()));
		
		//首先获取客户端IP地址
		String IP = sk.getInetAddress().getHostAddress();
		System.out.println("客户端IP地址为:"+IP);
		
		//使用while循环,当从客户端持续读取到数据的时候,循环,并打印读取到的数据,最后将数据使用输出流返回客户端
		String line = null;
		while((line = bufIn.readLine()) != null)
		{
	
			System.out.println("客户端发送的数据为:"+line);
			/*
			//再将数据转换为大写格式返回客户端
			bufOut.write(line.toUpperCase());
			bufOut.newLine();//与TcpClinet一样,这里要先换行再刷新输出
			bufOut.flush();//千万注意要将数据刷新并换行
			*/
			pw.println(line.toUpperCase());
			
		}
		sk.close();//关闭Socket服务
		ss.close();//关闭服务器端服务
	}
}

上传文本文件

TCP—客户端复制文件到服务端代码如下:

首先是客户端
package pack;
import java.net.*;
import java.io.*;

public class TcpClient {

	public static void main(String[] args) throws Exception
	{
		Socket sk = new Socket("192.168.1.195",20010);
		
		//我们先读取一个文件的数据到字符流缓冲区
		BufferedReader bufr = 
				new BufferedReader(new FileReader("G:\\Client.txt"));
		
		//接下来同样定义Socket的写入以及写出流
		BufferedReader bufIn = 
				new BufferedReader(new InputStreamReader(sk.getInputStream()));
		PrintWriter pw = new PrintWriter(sk.getOutputStream() , true);
		
		String line = null;
		//文件读取完毕则循环结束
		while((line = bufr.readLine()) != null)
		{
			pw.println(line);		
		}
		sk.shutdownOutput();//关闭客户端的输出流,相当于给流中加入一个结束标记-1.
		String feedbackInfo = bufIn.readLine();
		System.out.println("服务器端的反馈信息为:"+feedbackInfo);
				
		bufr.close();
		sk.close();//关闭Socket的输出流后还需要整体关闭Socket服务
	}
}
/*
结果1:发现数据已经成功复制到G盘下的“Server.txt”文件中,但是我们发现并没有“上传成功”的字样,而且2边的程序都在等待
(视频23-14,10.40开始解析)——服务器端的readLine方法不知道客户端已经发送完毕,因为客户端的输出流还在开启,
那么服务器端的readLine阻塞等待读取,它就不会将“上传成功”发回客户端。而客户端有读取feedbackInfo的readLine方法
其在等待服务器端的反馈信息!因此2边同样在等待。

(这里除了关闭流,还有2种设计自定义标记的方法——见23-14,13.00)

这里我们在循环结束之后要手动关闭客户端的输出流!(注意出现这种问题一般要分析阻塞式方法!阻塞式方法缺少结束标记!)
关闭Socket的输出流之后,相当于给流中加入一个结束标记-1,这样服务器端socket流的bufIn.readLine()底层调用read()方法
read()方法发现客户端输出流关闭了,它返回readLine也是-1,这样服务器端便也不再读取

上一个例子,因为是在循环里面接收反馈,而break跳出循环之后,执行Socket的关闭,服务器端的readLine方法底层调用socket流的read方法
发现socket流已经关闭,read()方法返回-1,那么readLine方法也会同样停止执行。(视频23-13,25.30解析)
*/

--------------------
其次是服务器端
package pack;
import java.net.*;
import java.io.*;

public class TcpServer {

	public static void main(String[] args) throws Exception
	{
		
		ServerSocket ss = new ServerSocket(20010);
		Socket sk = ss.accept();
		
		//接下来同样定义Socket的写入以及写出流
		PrintWriter pw = new PrintWriter(sk.getOutputStream() , true);	
		BufferedReader bufIn = 
				new BufferedReader(new InputStreamReader(sk.getInputStream()));
		
		//定义一个BufferedWriter 对象,将服务器接收到的数据写入一个文件
		BufferedWriter bufw = new BufferedWriter(new FileWriter("G:\\Server.txt"));
		//当然使用PrintWriter更加方便
//		PrintWriter out  = new PrintWriter(new FileWriter("G:\\Server.txt"),true);
		
		String IP = sk.getInetAddress().getHostAddress();
		System.out.println("客户端IP地址为:"+IP);
		
		String line = null;
		while((line = bufIn.readLine()) != null)
		{
			bufw.write(line);
//第一次我们没有添加刷新的时候,居然“Server.txt”也有内容?这是因为我们使用的是缓冲区,缓冲区是有大小的,当写入缓冲区的数据满了,它会自动刷新将数据写到文件,然后再读取数据到缓冲区!!!(35——day26——视频16,21.50)
			bufw.newLine();
			bufw.flush();
//			out.println(line);				
		}
		
		pw.println("上传成功");
		
		out.close();//这里要注意开了一个PrintWriter,要将其关闭
		sk.close();//关闭Socket会自动关闭其内部的pw与bufIn
		ss.close();
	}
}


上传图片

TCP-上传图片——一个客户端对一个服务器发送图片!

首先是客户端
---------------
package pack;
import java.net.*;
import java.io.*;

public class PicClient 
{
	public static void main(String[] args) throws Exception
	{
		Socket sk = new Socket("192.168.1.195",20011);
		
		//首先创建Socket服务的读取与写出流,由于是读取图片,我们这里直接使用字节读取流FileInputStream
//FileInputStream(String name) 通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name指定。
		FileInputStream fis = new FileInputStream("G:\\haha.png");
		
		//接下来,我们创建Socket流的读取与写出流,直接使用字节流
		OutputStream out = sk.getOutputStream();
		InputStream in = sk.getInputStream();
		
		//创建缓冲区,并将字节数据读入字节缓冲区,确定读入的数据的长度
		byte[] buf = new byte[1024];
		int len = 0;
		while((len = fis.read(buf)) != -1)
		{
			//利用Socket流的写出流将数据写出
			out.write(buf,0,buf.length);
			//对于没有指定缓冲区的字节流,数据会直接写入内存,不需要刷新(与字符流的区别,字符流需要刷新)
		}
		//关闭客户端的socket写出流,通过底层标记-1告诉服务端的read()方法,避免下面2个程序都在等待接收
		sk.shutdownOutput();
		
		//从服务端读取反馈
		byte[] feedbackBuf = new byte[1024];
		int feedbackLen = in.read(feedbackBuf);
		String feedbackInfo = new String(feedbackBuf,0,feedbackLen);
		System.out.println("服务端反馈信息为:"+feedbackInfo);
		
		fis.close();
		sk.close();
	}
}
---------------

其次是服务器端
package pack;
import java.net.*;
import java.io.*;

public class PicServer 
{

	public static void main(String[] args) throws Exception
	{
        //创建tcp的socket服务端。
		ServerSocket ss = new ServerSocket(20021);
        //获取客户端
		Socket sk = ss.accept();
		
		//获取客户端IP
		String ip = sk.getInetAddress().getHostAddress();
		System.out.println(ip+"-----connect");
	
		//同样创建Socket的读入写出流
		OutputStream out = sk.getOutputStream();
		InputStream in = sk.getInputStream();
		
		//同样,先创建FileOutputStream流,以便于将从客户端读取到的数据写入新的图片
		//这次我们先创建File对象,通过下面的代码来保证我们想存储到的目录一定存在
		File dir = new File("G:\\newlkj");
		if(!dir.exists())
		{//如果目录不存在,我们使用mkdirs方法创建该文件夹目录——包含必须但是不存在的父目录
			dir.mkdirs();
		}
		//在确保目录存在之后,我们可以创建自己想保存的文件
		File file = new File(dir,ip+".png");
		//然后同样,创建写出的流
		FileOutputStream fos = new FileOutputStream(file);
		
		byte[] buf = new byte[1024];
		int len = 0;
		//当服务端收到客户端发送的数据时,持续读取并写入新的图片文件
		while((len = in.read(buf)) != -1)
		{
			fos.write(buf, 0, buf.length);
		}
		//当客户端不再发生数据,说明图片读取完毕,这个时候客户端关闭其写出流,我们这里的read遇到标记-1,也会停止读取跳出循环
		//同时发送反馈数据给客户端
		out.write("传送完毕".getBytes());
		
		fos.close();
		sk.close();
		ss.close();
	}
}

多线程实现上传图片

接下来我们实现一下服务端的多线程功能,就是多个客户端向一个服务器端发送数据,这个时候服务器端就需要有多个线程同时来执行,如果只有一个线程,在同一时间只能接收到一个客户端的数据,这是不现实的。下面我们实现一下服务端的多线程接收程序。

首先是PicClient
package pack;
import java.net.*;
import java.io.*;

public class PicClient 
{
	/**
	 * @param args
	 * @throws UnknownHostException
	 * @throws IOException
	 */
	public static void main(String[] args) throws UnknownHostException, IOException 
	{
		//如果我们没有存入参数,会提示存入参数
		if(args.length!=1)
		{
			System.out.println("请选择一个jpg格式的图片");
			return ;
		}
		
		//我们发现,每一次我们客户端传递的图片都是指定的,不能修改,我们可以通过主函数传值的方法来实现修改想传递的图片
		File file = new File(args[0]);
		if(!(file.exists() && file.isFile()))
		{
			System.out.println("该文件有错误,文件不存在或者不是文件");
			return;//不符合条件就退出程序
		}
		if(!file.getName().endsWith(".png"))
		{
			System.out.println("文件格式错误");
			return;
		}
		//同样,服务端判断的文件大小也可以在这里判断
		if(file.length()>1024*1024*5)
		{
			System.out.println("文件过大,没安好心");
			return ;
		}
		
		
		//本机IP地址改变,导致第一次无法连接!
		Socket sk = new Socket("192.168.1.195",20205);
		
		FileInputStream fis = new FileInputStream(file);
		
		OutputStream out = sk.getOutputStream();
		InputStream in = sk.getInputStream();
		
		byte[] buf = new byte[1024];
		int len = 0;
		while((len = fis.read(buf)) != -1)
		{
			out.write(buf,0,len);
//当文件大于2m的时候,客户端会报错: Software caused connection abort: socket write error
//因为TCP协议下客户端和服务器端是相连的,一当服务器端因为文件过大停止这个IP地址相关线程,其socket关闭,但是该IP的客户端数据没有发送完毕,因此会报错
		}
		sk.shutdownOutput();
		
		byte[] feedbackBuf = new byte[1024];
		int feedbackLen = in.read(feedbackBuf);
		String feedbackInfo = new String(feedbackBuf,0,feedbackLen);
		System.out.println("服务端反馈信息为:"+feedbackInfo);
		
		fis.close();
		sk.close();
	}
}
-------------------
其次是PicServer
package pack;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

	
public class PicServer {

	/**
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException 
	{
		ServerSocket ss = new ServerSocket(20205);
		
//因为要接收多个客户端的信息,我们在服务端必须循环接收
//但是,如果只是简单循环接收,也只能一次性处理一个客户端的信息(见视频35-day27-19-5.00处解析)
//为了实现服务端可以同时处理多个客户端,我们一方面使用循环,使得服务端可以持续接收客户端的请求
//另一方面,在进循环获取到客户端的socket对象后,将下面的任务封装到多线程的run()方法里面,这样主线程就又会回到while循环的开头,
//当下一个客户端接到服务器,就可以获取它的socket对象,创建另一个线程,多线程同时执行!(见视频35-day27-19-9.00处解析)
		while(true)
		{
			//由于不同客户端对应不同的Socket流,因此我们服务端对象获取客户端Socket流的代码应该写在循环里面
			Socket sk = ss.accept();
			//启动线程——这里各个不同线程所对应的服务端代码都是一样的,
			//一个客户端链接上来就会创建线程并执行run()方法中的代码,就是将连接进来的客户端封装到一个线程当中。
			new Thread(new UploadTask(sk)).start();
		}
//while循环如果一直循环会导致死机,都是如果循环里面有阻塞式方法,循环就不会一直执行,必须满足条件阻塞式方法才会执行
	}

}
----------------
最后是UploadTask
package pack;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class UploadTask implements Runnable
{
	private static final int SIZE = 1024*1024*2;//定义一个2m大小的常量,用来限制接收到的文件的大小
	private Socket sk;
	//在构造方法为Socket赋予对象
	public UploadTask(Socket sk)
	{
		this.sk = sk;
	}
	@Override
	public void run() 
	{
		//在run方法内部定义一个计数器,用来保存一个IP地址向服务端发送同一个文件的次数
		//这个变量必须定义在run()方法内部,否则多个线程都会使用同一个变量
		int count = 0;
		//获取客户端IP地址
		String ip = sk.getInetAddress().getHostAddress();
		System.out.println(ip+"----connect");
		
		//接下来,有关于获取流的方法都会抛出IOException,run()方法里面不能抛出异常,必须在这里处理
		
		try
		{
			//确保文件夹存在,并在相应位置创建我们想创建的文件
			File dir = new File("G:\\newlkj");
			if(!dir.exists())
			{
				dir.mkdirs();
			}
			File file = new File(dir,ip+".png");
			
			//如果一个IP地址持续发送一个文件,我们计算将他发过来的文件计数命名,避免将之前上传的文件覆盖!
			//这里为什么用while不能用if,以及count为什么定义在run方法中,解析见视频25-day24-2,19.00解析
			while(file.exists())
			{//如果之前的file客户端发送过,我们就重新new这个file对象,持续发送多少次就new多少次对象
				file = new File(dir , ip+"("+(++count)+").png");
			}
			//创建一个FileOutputStream流将相应的数据写入我们想保存的文件
			FileOutputStream fos = new FileOutputStream(file);
			
			//创建一个socket流的文件读取与写出流
			OutputStream out = sk.getOutputStream();
			InputStream in = sk.getInputStream();
			
			int len = 0;
			byte[] buf = new byte[1024];
			while((len=in.read(buf)) != -1)
			{
				fos.write(buf,0,len);
			}
			//否则,如果写出成功,发送相应信息给客户端
			out.write("上传成功".getBytes());

			fos.close();
			sk.close();
			
		}
		catch(Exception e)
		{
			throw new RuntimeException(ip+"上传失败");		
		}		
	}

}

常见客户端与服务端

以及相应的原理——模拟服务器

最常见的客户端:
	浏览器 :IE。
最常见的服务端:
	服务器:Tomcat。
	提供网络资源访问的web服务器。Tomcat服务器用于处理各种客户端的请求并作出应答。

介绍了最常见的客户端(浏览器)与服务端(Tomcat服务器),那么他们又是如何运行的呢?我们进行以下操作

image-20230213203431248

了解客户端与服务端原理

首先,我们模拟客户端MyTomcat
package pack;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

public class MyTomcat {

	/**
	 * @param args
	 * @throws IOException 
	 * @throws UnknownHostException 
	 */
	public static void main(String[] args) throws UnknownHostException, IOException
	{
		//服务器
		ServerSocket ss = new ServerSocket(21001);
		Socket sk = ss.accept();
		
		System.out.println(sk.getInetAddress().getHostAddress()+".....connected");
		
		//创建读取流,读取客户端发送改过来的数据
		InputStream in = sk.getInputStream();
		byte[] buf = new byte[1024];
		int len = in.read(buf);
		String str = new String(buf,0,len);
		System.out.println(str);

		//使用PrintWriter将反馈信息发回客户端
		PrintWriter out = new PrintWriter(sk.getOutputStream(),true);
		out.println("<font color='red' size='7'>欢迎光临</font>");
		
		sk.close();
		ss.close();
	}
}

我们使用IE浏览器作为客户端,如下图,我们使用浏览器访问21001端口

在这里插入图片描述

客户端接收到服务器端“欢迎光临”的反馈,而服务器端收到的信息如下:(35-day27-21-14.00解析)

192.168.1.195.....connected
下面是客户端发送的请求
GET / HTTP/1.1   
这一行叫做请求行,包括:请求方式  /myweb/1.html—请求的资源路径(这里没有标出)   http协议版本。
下面的内容叫做:请求消息头, 属性名:属性值
这部分是客户端告诉服务端自己允许使用什么应用程序来解析(35-day27-21-17:30解析)
Accept: image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, application/x-ms-xbap, */*(支持解析的文件格式)
Accept-Language: zh-CN(语言)
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; Tablet PC 2.0; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)
Accept-Encoding: gzip, deflate(压缩方式)
Host: 192.168.1.195:21001
Connection: Keep-Alive
//空行
//请求体。
其实在请求行与请求头下面,还可以通过请求体加入自己的自定义内容。

同一个IP地址下面可以有不同的应用程序,我们也可以在IP地址下面自定义主机的应用程序,如下,我们在“C:\Windows\System32\drivers\etc”路径下,使用管理员权限访问hosts文件,输入应用程序的URL与IP地址的对应关系,在浏览器访问服务器应用程序URL与访问服务器IP地址,返回的反馈信息是一样的。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

另外,关于正斜杠“/”与反斜杠的“\”的区别,见下面文章

正斜杠与反斜杆区别

模拟一个浏览器获取信息

http超文本传输协议

html超文本标记语言

image-20230213223926867

首先,我们模拟浏览器客户端
package pack;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;

public class MyBrowser {

	/**
	 * @param args
	 * @throws IOException 
	 * @throws UnknownHostException 
	 */
	public static void main(String[] args) throws UnknownHostException, IOException 
	{
		//注意,视频中老师安装了Tomcat服务器,因此视频中不需要自己定义服务器,直接使用Tomcat的8080端口
		//而/myweb/1.html资源是Tomcat服务器中才有的资源,这里搞不出来,这个程序事实上没办法访问,主要看一下视频怎么搞。
		//模拟浏览器,给tomcat服务端发送符合http协议的请求消息。
		Socket sk = new Socket("196.168.1.195",8080);

		//首先使用PrintWriter写出信息
		PrintWriter out = new PrintWriter(sk.getOutputStream(),true);
		out.println("Get /myweb/1.html HTTP/1.1");//请求行
		out.println("Accept: */*");//下面是请求消息头
		out.println("Host: 192.168.1.195:21005");
		out.println("Connection: close");
		out.println();
		
		//下面接收服务器端的反馈信息
		InputStream in = sk.getInputStream();
		int len = 0;
		byte[] buf = new byte[1024];
		String text = new String(buf,0,len);
		System.out.println("服务器端反馈信息"+text);
		sk.close();
	}
}

服务端发回应答消息。
HTTP/1.1 200 OK  
 //应答行,http的协议版本   应答状态码   应答状态描述信息
 几个常见的应答状态码:
 200:客户端的请求服务器端解析成功
 404:not found,客户端的请求没有找到

应答消息头
应答消息属性信息。 属性名:属性值
Server: Apache-Coyote/1.1
ETag: W/"199-1323480176984"
Last-Modified: Sat, 10 Dec 2011 01:22:56 GMT
Content-Type: text/html
Content-Length: 199
Date: Fri, 11 May 2012 07:51:39 GMT
Connection: close
//空行(注意必须加这一行空行)
//应答体。
<html>
	<head>
		<title>这是我的网页</title>
	</head>

	<body>

		<h1>欢迎光临</h1>

		<font size='5' color="red">这是一个tomcat服务器中的资源。是一个html网页。</font>
	</body>
</html>

需要搞清楚,客户端往服务端发送请求,而服务端发回客户端应答。

URL&URLConnection

  • URL表示统一资源定位符
  • 抽象类URLConnection是表示应用程序和URL之间的通信链接的所有类的超类。 该类的实例可以用于从URL引用的资源中读取和写入。

我们制作的浏览器在接收服务器的应答信息的时候会显示那么多内容,而真正的浏览器只显示相应的应答数据(解析了应答数据),这是因为IE浏览器有HTTP协议的解析引擎。我们应该如何实现这种功能——见(35-day27-23-0.00解析)
  我们之前使用的Socket或者DatagramSocket都是在传输层,而我们想实现HTTP的解析,必须在应用层。那么我们必须解析URL,例如“http://www.lkj.com:21001/”,解析这个URL需要使用Java中的URL类(URL与URI区别见35-day27-23-11.40解析)
URI&URL&URN
  注意,我们现在讨论的URL是在应用层的层面讨论!而URL是客户端浏览器用于解析从服务器端获取的应答消息体

注意,这一部分因为我们没有安装Tomcat服务器,因此我们没有办法在eclipse里面实现,主要看视频如何实现。

package pack;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

public class URLDemo {

	/**
	 * @param args
	 * @throws IOException 
	 */
	public static void main(String[] args) throws IOException {
		//这一部分,见(35-day27-23-16.40解析)
		//首先,创建一个URL的字符串,并将该字符串作为URL类的参数,创建URL对象
		//URL(String spec)根据 String表示形式创建 URL对象。
//		String str_url = "http://192.168.1.100:8080/myweb/1.html?name=lisi";
		String str_url = "http://192.168.1.100:8080/myweb/1.html";
		//事实上,我们之前在传输层,只将“192.168.1.100:8080”这两部分封装成为Socket对象
		URL url = new URL(str_url);
		
		/*
		//先试着获取一些信息
		System.out.println("getProtocol:"+url.getProtocol());//此URL的协议名称:getProtocol:http
		System.out.println("getHost:"+url.getHost());//此URL的主机名:getHost:192.168.1.100
		System.out.println("getPort:"+url.getPort());//此 URL 的端口号:getPort:8080
		System.out.println("getFile:"+url.getFile());//此 URL 的文件名。:/myweb/1.html?name=lisi(文件包括文件路径与参数信息)
		System.out.println("getPath:"+url.getPath());//此 URL 的路径部分。:getPath:/myweb/1.html(指的是要去URL的文件路径)
		System.out.println("getQuery:"+url.getQuery());//此 URL 的查询部分。:getQuery:name=lisi(指的是文件参数信息)
		*/
		
		//下面试着使用URL对象的功能
//InputStream openStream() 打开到此 URL 的连接并返回一个用于从该连接读入的 InputStream。 
//这个方法可以链接到此URL对象所指向的连接,并获取此连接的内容
		InputStream in = url.openStream();
		byte[] buf = new byte[1024];
		int len = in.read(buf);	
		String text = new String(buf,0,len);	
		System.out.println(text);		
		in.close();
/* 
 注意,“"http://192.168.1.100:8080/myweb/1.html"”这一句指的是客户端发送给服务器端的请求,客户端向服务器要/myweb/1.html信息
 服务器的IP地址是192.168.1.100,端口是8080,。
 如果我们在客户端想获得服务器端应答的"/myweb/1.html"的内容,而不是服务器端发回的所有信息,
 我们只需要使用URL对象解析"http://192.168.1.100:8080/myweb/1.html"这一句即可
 
 结果:因为我们没有像视频中一样安装Tomcat服务器,因此不存在myweb/1.html这个网页的内容,那么也就无法模拟视频中的运行,具体看视频怎么搞即可
 获取到了服务器端的应答数据体。
 <html>
	<head>
		<title>这是我的网页</title>
	</head>

	<body>

		<h1>欢迎光临</h1>

		<font size='5' color="red">这是一个tomcat服务器中的资源。是一个html网页。</font>
	</body>
</html>
 */
		//这样,我们在创建自己的客户端浏览器的时候,只需要在MyBroser中加入URL对象,就可以解析只获得服务器的应答消息体。
//当然,客户端浏览器通过"http://192.168.1.100:8080/myweb/1.html"向服务器获取相应的信息,必须等服务器响应返回/myweb/1.html后
//客户端浏览器才可以通过“URL”来解析"http://192.168.1.100:8080/myweb/1.html"并获取应答体内容
		
		//那么URL是如何解析的呢?见(35-day27-23-26.40解析)
		//首先,获取一个URLConnection,既URL连接器对象,将连接封装成为对象
		//openStream()方法的底层是:openConnection().getInputStream()
		URLConnection conn = url.openConnection();
		InputStream in1 = conn.getInputStream();//通过这种方法同样可以获得URL中消息体的读取流
		//上面这两句其实是openStream的原理
		
		
//		System.out.println(conn);
/*结果:sun.net.www.protocol.http.HttpURLConnection:http://192.168.1.100:8080/myweb/1.html(35-day27-23-28.40解析)
上面这个是一个包,这个是java制作者提供的HTTP协议的底层实现,这个包不对外提供,它封装了HTTP协议的解析方式。
提供URL类的openConnection方法获取到HTTP协议的解析包,对相应的内容进行解析,然后再使用getInputStream()方法获取的解析信息的输出流
外部就可以通过输出流来获取到解析的协议。

这个URLConnection对象其实是将连接封装成了对象,它是java中内置的可以解析的具体协议的对象+socket.
*/
		/*
		//下面看一下URLConnection的方法
		//String getHeaderField(String name) 返回指定的头字段的值。 
		String value = conn.getHeaderField("Content-Type");
		System.out.println(value);
		//结果:text/html,既获取到URL所指向的信息中应答消息头中相应的信息
		*/		
	}

}

这个URLConnection对象其实是将连接封装成了对象,它是java中内置的可以解析的具体协议的对象+socket。这样,下次客户端就不需要在写Socket,只需要通过URL来获取URLConnection对象获取连接即可。(也就是说URL不仅仅将Socket的传输功能包含进来,而且还包含有解析通过Socket传输的相应数据的对象,可以将传输的数据进行解析
  为什么说可以不使用Socket,因为我们想要从服务端获取的内容,如“/myweb/1.html”,与要访问的服务端的IP以及端口,如192.168.1.100:8080封装成为URL对象,获取URL的连接对象URLConnection,就可以获取URL的InputStream与OutputStream,这样就可以向服务端发送数据,也可以从服务端接收客户端想要的数据,不需要Socket。

常见网络结构

常见的网络结构如下:

1、C/S  client/server
	特点:
		该结构的软件,客户端和服务端都需要编写。
		可发成本较高,维护较为麻烦。		
	好处:
		客户端在本地可以分担一部分运算。例如:主机游戏

2、B/S  browser/server
	特点:
		该结构的软件,只开发服务器端,不开发客户端,因为客户端直接由浏览器取代。 
		开发成本相对低,维护更为简单。
	缺点:所有运算都要在服务端完成。例如:网页游戏

olor=“red”>这是一个tomcat服务器中的资源。是一个html网页。

*/ //这样,我们在创建自己的客户端浏览器的时候,只需要在MyBroser中加入URL对象,就可以解析只获得服务器的应答消息体。 //当然,客户端浏览器通过"http://192.168.1.100:8080/myweb/1.html"向服务器获取相应的信息,必须等服务器响应返回/myweb/1.html后 //客户端浏览器才可以通过“URL”来解析"http://192.168.1.100:8080/myweb/1.html"并获取应答体内容
	//那么URL是如何解析的呢?见(35-day27-23-26.40解析)
	//首先,获取一个URLConnection,既URL连接器对象,将连接封装成为对象
	//openStream()方法的底层是:openConnection().getInputStream()
	URLConnection conn = url.openConnection();
	InputStream in1 = conn.getInputStream();//通过这种方法同样可以获得URL中消息体的读取流
	//上面这两句其实是openStream的原理

// System.out.println(conn);
/*结果:sun.net.www.protocol.http.HttpURLConnection:http://192.168.1.100:8080/myweb/1.html(35-day27-23-28.40解析)
上面这个是一个包,这个是java制作者提供的HTTP协议的底层实现,这个包不对外提供,它封装了HTTP协议的解析方式。
提供URL类的openConnection方法获取到HTTP协议的解析包,对相应的内容进行解析,然后再使用getInputStream()方法获取的解析信息的输出流
外部就可以通过输出流来获取到解析的协议。

这个URLConnection对象其实是将连接封装成了对象,它是java中内置的可以解析的具体协议的对象+socket.
/
/

//下面看一下URLConnection的方法
//String getHeaderField(String name) 返回指定的头字段的值。
String value = conn.getHeaderField(“Content-Type”);
System.out.println(value);
//结果:text/html,既获取到URL所指向的信息中应答消息头中相应的信息
*/
}

}


这个URLConnection对象其实是将连接封装成了对象,它是java中内置的可以解析的具体协议的对象+socket。这样,下次客户端就不需要在写Socket,只需要通过URL来获取URLConnection对象获取连接即可。(**也就是说URL不仅仅将Socket的传输功能包含进来,而且还包含有解析通过Socket传输的相应数据的对象,可以将传输的数据进行解析**)
  为什么说可以不使用Socket,因为我们想要从服务端获取的内容,如“/myweb/1.html”,与要访问的服务端的IP以及端口,如192.168.1.100:8080封装成为URL对象,获取URL的连接对象URLConnection,就可以获取URL的InputStream与OutputStream,这样就可以向服务端发送数据,也可以从服务端接收客户端想要的数据,不需要Socket。

## 常见网络结构

常见的网络结构如下:

1、C/S client/server
特点:
该结构的软件,客户端和服务端都需要编写。
可发成本较高,维护较为麻烦。
好处:
客户端在本地可以分担一部分运算。例如:主机游戏

2、B/S browser/server
特点:
该结构的软件,只开发服务器端,不开发客户端,因为客户端直接由浏览器取代。
开发成本相对低,维护更为简单。
缺点:所有运算都要在服务端完成。例如:网页游戏


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值