计算机网络

网络编程的目的就是直接或间接地通过网络协议与其他计算机进行通信。在 Java 语言中包含网络编程所需要的各种类,编程人员只需要创建这些类的对象,调用相应的方法,就可以进行网络应用程序的编写。
要进行网络程序的编写,编程人员需要对下面5个方面的基础知识有一定的了解:
1.计算机网络
2.网络分类
3.网络编程模式
4.套接字
5.网络通信协议
稍微具体的内容:
1.计算机网络
指将地理位置不同的具有独立功能的多台计算机及其外部设备,
通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,
实现资源共享和信息传递的计算机系统。
2.网络分类
按照地理范围主要将网络分为局域网、城域网、广域网和因特网。
(1) 局域网(LocalArea Network)简称 LAN,是一种在小范围内实现的计算机网络,一般在一个建筑物内或者一个工厂、一个事业单位内部独有,范围较小。
(2) 城域网(Metropolitan Area Network)简称为 MAN,一般是一个城市内部组建的计算机信息网络,提供全市的信息服务。
(3) 广域网(Wide Area Network)简称为 WAN,它的范围很广,可以分布在一个省、一个国家或者几个国家。
(4) 因特网(Internet)则是由无数的 LAN 和 WAN 组成的。
3.网络编程模式
在网络通信中主要有两种模式的通信方式:
一种是客户机/服务器(Client/Server)模式,简称为 C/S 模式;
另一种是浏览器/服务器(Browser/Server)模式,简称 B/S 模式。
Client/Server 模式
客户机、服务器以及网络三者之间的关系图,使用这种模式的程序很多,例如很多读者喜欢玩的网络游戏,需要在本机上安装一个客户端,服务器运行在游戏开发公司的机房。
使用 C/S 模式的程序,在开发时需要分别针对客户端和服务器端进行专门开发。
这种开发模式的优势在于由于客户端是专门开发的,表现力会更强,缺点就是通用性差,
也就是说一种程序的客户端只能和对应的服务器端进行通信,不能和其他的服务器端进行通
信,在实际维护中,也需要维护专门的客户端和服务器端,维护的压力较大。
Browser/Server 模式
对于很多程序,运行时不需要专门的客户端,而是使用通用的客户端,例如使用浏览器。用户使用浏览器作为客户端的这种模式叫作浏览器/服务器模式。使用这种模式开发程序时只需开发服务器端即可,开发的压力较小,不需要维护客户端。但是对浏览器的限制比较大,表现力不强。
4.套接字
在网络上很多应用程序都是采用客户端/服务器(C/S)的模式,实现网络通信必须将两台计算机连接起来建立一个双向的通信链路,这个双向通信链路的每一端在Java的网络编程中,我们又称之为一个套接字(Socket)。套接字(Socket)是一个抽象出来的概念代表在端上的通信链路。
5.网络协议
网络上的计算机之间要通信,就必须有一些约定,这些约定被称为网络通信协议。
就像我们说话用某种语言一样,在网络上的各台计算机之间也有一种语言,这就是通信协议,不同的计算机之间必须使用相同的通信协议才能进行通信。
通信协议是由三个要素组成:
(1) 语义。语义是解释控制信息每个部分的意义。它规定了需要发出何种控制信息,以及完成的动作与做出什么样的响应。
(2) 语法。语法是用户数据与控制信息的结构与格式,以及数据出现的顺序。
(3) 时序。时序是对事件发生顺序的详细说明。(也可称为“同步”)。
---人们形象地把这三个要素描述为:语义表示要做什么,语法表示要怎么做,时序表示做的顺序。
通信协议是网络上的设备进行网络通信的基础,这些具体的协议是非常多的,
在Java的网络编程这部分知识的学习中,我们必须要了解的有关的主要协议,TCP/IP 和HTTP
(1) TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议):
TCP/IP协议是一个协议集合。大家叫的时候方便说,所以统称为TCP/IP。
在 TCP/IP 中包含一系列用于处理数据通信的协议:
A...TCP (传输控制协议) - 应用程序之间通信
B...UDP (用户数据包协议) - 应用程序之间的简单通信
C...IP (网际协议) - 计算机之间和计算机网络之间的通信
D...ICMP (因特网消息控制协议) - 针对错误和状态
E...DHCP (动态主机配置协议) - 针对动态寻址
IP协议:
IP(Internet Protocol)协议,又称网际协议,是TCP/IP协议的核心。
它负责Internet上网络之间的通信,并规定了将数据报从一个网络传输到另一个网络所应遵循的规则。具体来说,IP协议不但定义了数据传输时的基本单元和格式,还定义了数据报的递交方法和路由选择。此外,在TCP/IP网络中,主机之间进行通信所必需的地址,也是通过IP协议来实现的。
TCP协议:
TCP 用于应用程序之间的通信。
当应用程序希望通过 TCP 与另一个应用程序通信时,它会发送一个通信请求。这个请求必须被送到一个确切的地址。在双方“三次握手”之后,
TCP 将在两个应用程序之间建立一个全双工 (full-duplex) 的通信。
这个全双工的通信将占用两个计算机之间的通信线路,直到它被一方或双方关闭为止。
TCP三次握手四次挥手
TCP三次握手
所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:

image.png

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。
1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
---这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。

image.png

UDP协议:
Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP,UserDatagram Protocol)。使用UDP协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;
在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
UDP协议用的场合如:微信的语音, QQ直播课等。
UDP与TCP简单对比:
时间上,UDP,无连接,不存在建立连接需要的时延,TCP建立链接会产生时延。
空间上,TCP需要在端系统中维护连接状态,需要一定的开销。
此连接状态包括接收和发送缓存,拥塞控制参数和序号与确认号的参数。
UCP不维护连接状态,也不跟踪这些参数,开销小。空间和时间上都具有优势。可靠性上,TCP面向链接的通信可靠性高,UDP可靠性差。
实际应用中,TCP应用多很多。如qq它是两协议一起用。
HTTP协议
HTTP是HyperText Transfer Protocol的缩写,翻译过来就是超文本传输协议,它是一种用于分布式、协作式和web应用系统的应用层协议。
既然前面讲了TCP/UDP的网络通信协议,而且他们的使用是非常广泛的,那为啥有多出个http协议来呢?
UDP协议具有不可靠性和不安全性,显然这很难满足web应用的需要。而TCP协议是基于连接和三次握手的,虽然具有可靠性,但任然具有一定的缺陷。试想一下,B/S架构的网站,十万人同时在线也是很平常的事儿。如果十万个客户端和服务器一直保持连接状态,那服务器那端有这么多带宽来分配吗?
这就衍生出了http协议。基于TCP的可靠性连接。通俗点说,就是在请求之后,服务器端立即关闭连接、释放资源。这样既保证了资源可用,也吸取了TCP的可靠性的优点。
HTTP工作原理
HTTP协议定义Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。
HTTP协议采用了请求/响应模型。
客户端向服务器发送一个请求报文,请求报文包含请求行、请求头部、空行和请求数据。
服务器收到客户端的请求会响应,响应的内容包括状态行、响应头部、空行和响应数据。
以下是 HTTP 请求/响应的步骤:
1.浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址,客户端连接到Web服务器一个HTTP客户端,通常是浏览器,与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。
2.发送HTTP请求
通过TCP套接字,客户端向Web服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据4部分组成。
3.服务器接收请求并返回HTTP响应
Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据4部分组成。
4.释放连接TCP连接
若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;
若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
5.客户端浏览器解析HTML内容
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。
然后解析每一个响应头,响应头告知HTML文档的若干特殊信息和文档的字符集。
客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。
HTTP请求报文(就是浏览器首先发送给服务器的信息)HTTP请求格式,如下图:

image.png

HTTP请求方法
HTTP/1.1协议中共定义了八种方法(也叫“动作”,常用的为GET,POST)来以不同方式操作指定的资源:
GET
向指定的资源发出“显示”请求。
HEAD
类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头。
POST
向指定资源提交数据,请求服务器进行处理(例如提交表单或者上传文件)。数据被包含在请求数据中。这个请求可能会创建新的资源或修改现有资源,或二者皆有。
PUT
向指定资源位置上传其最新内容。
DELETE
请求服务器删除Request-URI所标识的资源。
TRACE
回显服务器收到的请求,主要用于测试或诊断。
OPTIONS
这个方法可使服务器传回该资源所支持的所有HTTP请求方法。用'*'来代替资源名称,向Web服务器发送OPTIONS请求,可以测试服务器功能是否正常运
作。
CONNECT
HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接(经由非加密的HTTP代理服务器)。
URL ( Uniform Resource Locator )
----超文本传输协议(HTTP)的 统一资源定位符将从 因特网获取信息的基本元素包括在一个简单的地址中。
URL 的具体语法格式如下所示
协议://用户名:密码@子域名.域名.顶级域名:端口号/目录/文件名.文件后缀?参数=值&参数2=值2....#标志
下面是一些简单的 URL 示例:

http://www.sun.com/ 协议名://主机名
http://localhost:8080/Test/admin/login.jsp 协议名://机器名:端口号/文件名

头部:
1、Accept,浏览器端能够处理的信息内容类型。
Accept: 文件类型使用的MIME值,*/*代表任意类型MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准。不用记,用哪个查那个,百度:MIME 参考手册

注意:
  docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document
  xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
  pptx,application/vnd.openxmlformats-officedocument.presentationml.presentation
ie 会把 jpg 、 jpeg 翻译成 image/pjpeg , png 翻译成 image/x-png

2、Accept-Encoding, 浏览器能够处理的的压缩编码。
通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate),(注意:这不是指字符编码)。
例如: Accept-Encoding:gzip, deflate, br
3、Accept-Language, 浏览器当前设置的语言。
语言跟字符集的区别:中文是语言,中文有多种字符集,比如big5,gb2312,gbk等等;
4、Accept-Charset:浏览器能够显示的字符集
5、Connection:浏览器与服务器的连接类型
例如:Connection: keep-alive 当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,
如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。
例如: Connection: close 代表一个Request完成后,客户端和服务器之间用于传输HTTP数据的TCP连接会关闭。
当客户端再次发送Request,需要重新建立TCP连接。
6、Host,发送请求的页面的域名。
7、Referer,发送请求的页面的URI。当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器借此可以获得一些信息用于处理。
8、User-Agent,浏览器的用户代理字符串。告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本。
9、Cookie,用来存储一些用户信息以便让服务器辨别用户身份的。
10、Cache-Control,指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据,还是重新发请求到服务器获取数据。
常看到的:max-age=0,也就是这些资源在浏览器缓存中,保存的时间,值的单位是秒。
HTTP响应消息(服务器返回给浏览器的):
HTTP响应格式

image.png

响应头(消息头)包含:
1、Location:这个头配合302状态吗,用于告诉客户端找谁
2、Server:服务器通过这个头,告诉浏览器服务器的类型
3、Content-Encoding:告诉浏览器,服务器的数据压缩格式
4、Content-Length:告诉浏览器,回送数据的长度
5、Content-Type:告诉浏览器,回送数据的类型
6、Last-Modified:告诉浏览器当前资源缓存时间
7、Refresh:告诉浏览器,隔多长时间刷新
8、Content-Disposition:告诉浏览器以下载的方式打开数据。
例如: context.Response.AddHeader("Content-Disposition","attachment:filename=aa.jpg");
context.Response.WriteFile("aa.jpg");
9、Transfer-Encoding:告诉浏览器,传送数据的编码格式
10、ETag:缓存相关的头(可以做到实时更新)
11、Expries:告诉浏览器回送的资源缓存多长时间。如果是-1或者0,表示不缓存
12、Cache-Control:控制浏览器不要缓存数据 no-cache
13、Pragma:控制浏览器不要缓存数据 no-cache
14、Connection:响应完成后,是否断开连接。 close/Keep-Alive
15、Date:告诉浏览器,服务器响应时间
16、状态行: 例如: HTTP/1.1 200 OK (协议的版本号是1.1 响应状态码为200响应结果为 OK)
17、实体内容(实体头):响应包含浏览器能够解析的静态内容,例如:html,纯文本,图片等等信息

Java的基本网络支持

------Java为网络编程提供了支持包java.net,该包下提供了InetAddress、ULRDecoder、URLEncoder、URL和URLConnection等等很多工具类,这些类可以让我们通过编程的形式访问Web服务器或者其他的服务器。
一,InetAddress类及其常用方法
Internet 上的主机有两种方式表示地址,分别为域名和 IP 地址。
java.net 包中的 InetAddress 类对象包含一个 Internet 主机地址的域名和 IP 地址。
InetAddress 类提供了操作 IP 地址的各种方法。
该类本身没有构造方法,而是通过调用相关静态方法,比如getByName()方法获取实例。
InetAddress 类中的常用方法如下表 所示。

image.png

编写程序练习 InetAddress 类的基本使用方法:
(1) 创建一个类。
在 main() 方法中创建一个 InetAddress 对象,调用 getByName() 方法并传递参数“www.qq.com”输出此对象的 IP 地址字符串和主机名,代码如下所示。

public static void main(String[] args)
{
   try
    {
      InetAddress ia1=InetAddress.getByName("www.qq.com");  
     System.out.println(ia1.getHostName());//www.qq.com
     System.out.println(ia1.getHostAddress());//ip地址
   }
 catch(UnknownHostException e)
   {
     e.printStackTrace();
    }
}

(2) 在 main() 方法中添加代码,创建一个 InetAddress 对象,调用 getByName() 方法并传递参数“61.135.169.105”输出此对象的 IP 地址字符串和主机名,代码如下所示:

public class Demo1 {
    public  static void main(String[] args)  {
        try {
            InetAddress ia1 = InetAddress.getByName("www.qq.com");
            InetAddress ia2= InetAddress.getLocalHost();              //本地主机信息对象
            //InetAddress ia1= InetAddress.getByName("58.246.163.58");
            System.out.println(ia1.getHostName());   //www.qq.com
            System.out.println(ia1.getHostAddress());//ip数字地址
            System.out.println("本地计算机名:"+ia2.getHostName());
            System.out.println("本地主机ip:"+ia2.getHostAddress());
        }catch (UnknownHostException e){
            e.printStackTrace();
        }


    }
}

注意:在上述代码中包含互联网的地址,所以运行时需要连网,否则会出现异常。
二,ULRDecoder和URLEncoder
ULRDecoder和URLEncoder的作用主要是实现:
普通字符串和 application/x-www-form-urlencoded字符串相互转换的功能。
application/x-www-form-urlencoded: 是一种应用在网页提交数据的过程中,被提交的数据的编码格式,一般是汉字转换(其它不知),它的基本格式:%XX%XX%XX, XX为十六进制的数字。
例:
1---

image.png

2---
我们把地址东西复制下来粘到一个记事本中:可以看到‘’wd=熊少文‘’变成如下形式
image.png

除了英文字毒,数这,括符等合法字符,其他字符出现在 URL 之中都必须转义,规则是根据操作系统的默认编码,将每个字节转为百分号(%)加上两个大写的十六进制字母。比如,UTF-8 的操作系统上,http://www.example.com/q=春节这个 URL 之中,汉字“春节”不是 URL 的合法字符,所以被浏览器自动转成http://www.example.com/q=%E6%98%A5%E8%8A%82。
这是因为“春”和”节“的 UTF-8 编码分别是E6 98 A5和E8 8A 82,
将每个字节前面加上百分号,就构成了 application/x-www-form-urlencoded格式字符串。
----下面代码:分别把熊少文,编码与反码。

public class Demo2 {
    public static void main(String[] args) {
        try {
            // 将普通字符创转换成application/x-www-from-urlencoded字符串
            String urlString = URLEncoder.encode("熊少文", "UTF-8"); //输出 %E7%86%8A%E5%B0%91%E6%96%87
            System.out.println(urlString);
            // 将application/x-www-from-urlencoded字符串转换成普通字符串
            String keyWord = URLDecoder.decode("%E7%86%8A%E5%B0%91%E6%96%87", "UTF-8");
            System.out.println(keyWord); //输出你好

        } catch (UnsupportedEncodingException e) {

            e.printStackTrace();
        }
    }
}

三,URL类和URLConnection类
URL类
在 java.net 包中包含专门用来处理 URL 的 URL类,可以获得 URL 的相关信息,例如 URL 的协议名和主机名等。下面分别对它的构造方法和常用方法进行介绍。

image.png

URL 的常用方法:
image.png

URLConnection 类
----完成了 URL 的定义,接下来就可以获得 URL 的通信连接。在 java.net 包中,定义了专门的 URLConnection 类来表示与 URL 建立的通信连接,URLConnection 类的对象使用 URL 类的 openConnection() 方法获得。
注意:我们在应用程序和URL链接的时候,使用URLConnection类,当我们HTTP和URL链接的时候,使用URLConnection类的子类HttpURLConnection。
URLConnection 类的主要方法:
image.png

示例:获取百度首页的html内容。

/**
 *  URL url指向百度首页,连接之,打印输入流
 */
public class Demo3 {
    public static void main(String[] args) {
        try{
            URL url = new URL("https://www.baidu.com/");
            System.out.println(url.getProtocol());
            System.out.println(url.getHost());
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();  //这里用到http与应用连接
            //正式和http://xxxx/对应的服务器TCP链接
            conn.connect();
            InputStream in =conn.getInputStream();  //这个方法,就会向服务器发送请球,服务器收到这个请求后,会给我们一个HTTP响应,响应包含:头,正文
            byte[] buffer =new byte[1024];
            int hasRead=0;
            while((hasRead=in.read(buffer))!=-1){
                System.out.println(new String(buffer,0,hasRead));
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

image.png

网络类综合应用-下载远端文件。

----这里我们用四个线程,去下载。
1下载工具类

public class DownUtil {
    //首先需要的东西,要知道下载的目标文件的URL
    private String urlPath;
    //确定保存文件的位置
    private String targetFile;
    //确定下载用几条线程
    private int threadNum;
    //定义一个数组,放的是下载的线程类
    private DownThread[] threads;
    //把用于下载的线程类定义为工具里的内部类
    private class DownThread extends Thread{
        //线程类里边的具体实现
        private int startPos;   //开始位置(下载)
        private int currentPartSize;//当前负责的块大小
        private RandomAccessFile currentPart;  //当前下载的文件块
        //记录每条线程已经下载下来的文件的字节数
        private int length;
        public DownThread(int startPos,int currentPartSize,RandomAccessFile currentPart) {
            this.startPos = startPos;
            this.currentPartSize = currentPartSize;
            this.currentPart = currentPart;
        }
        @Override
        public void run() {
            
            try {        //类内部异常不可直抛出
                URL url = new URL(urlPath);
                HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
                //真正链接目标之前,要设置头部信息。
                conn.setConnectTimeout(5000);  //5秒后不链接
                conn.setRequestMethod("GET");   //GET就够了,只是一个字符串(文件名)请求。
                conn.setRequestProperty("Accept", "*/*");   //客户端可处理任何文件
                conn.setRequestProperty("Accept-Language", "zh-CN");
                conn.setRequestProperty("Charset", "UTF-8");
                conn.setRequestProperty("Connection", "Keep-Alive"); //保持TCP连接暂时不断
                //前面设置这么多东西,一个目的。取得目标文件的大小
                //链接上目标
                conn.connect();   //可不写
                InputStream in=conn.getInputStream();
                //把in的指针,跳到该线程负责下载的位置
                in.skip(this.startPos);
                byte[] buffer =new byte[1024];
                int hasRead =0;
                while(length

下载测试类Demo4.java
如果在上面的实现基础上,要实现断点下载,只需要额外添加一个配置文件,配置文件
中记录一下每个线程下载到那个字节,断网后再次下载的时候,只需要继续从上次下载
的位置继续下载就可以了。


public class Demo4 {
  public static void main(String args[]) throws Exception {
      DownUtil downUtil = new DownUtil("https://qcloud.dpfile.com/pc/47nSY2Xm9PZfaVzDfnkvcikxJqMbf0z-B4uD9sQCvj-OOBtfzYHS2nOTLYzTtJMtjoJrvItByyS4HHaWdXyO_I7F0UeCRQYMHlogzbt7GHgNNiIYVnHvzugZCuBITtvjski7YaLlHpkrQUr5euoQrg.jpg","c:\\xiong/aaa.jpg", 5);
      downUtil.download();  //下载
      new Thread(new Runnable() {
        
        @Override
        public void run() {
            while(downUtil.getCompleteRate()<=1) {
                System.out.println("已下载:  "+downUtil.getCompleteRate());
                try {
                    Thread.sleep(10);
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
            
        }
    }).start();
  }
}

在来一个案例:javanetModule1---Demo5.java
-----前面案例是发送GET请求,然后这里我们再来实现一个POST请求:

**
 * 该类功能:测试:post ,get请求服务器的区别(web服务器是:springmvc项目:qihangkttest,IndexController.java index1.jsp)
 * @author x
 *
 */
public class Demo5 {
   public static void main(String args[]) throws Exception {
       String urlPath = "http://localhost/qihangkt/postIndex.html";
       //要提交给服务器的数据,汉字要编码(
       String param = "name="+URLEncoder.encode("熊少文","UTF-8");
       //建立连接
       URL url = new URL(urlPath);
       HttpURLConnection conn= (HttpURLConnection) url.openConnection();
       //设置参数
       conn.setDoOutput(true);   //post请求,它是把数据塞到https协议中报文中,所以我们要设置true允许修改输出。
       conn.setDoInput(true);
       conn.setUseCaches(false);
       conn.setRequestMethod("POST");
       
       
       //设置请求头信息
       conn.setRequestProperty("Charset", "UTF-8");
       conn.setRequestProperty("Connection", "Keep-Alive"); //长连接
       conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
       
       //获取到输出流,才能把param放到请求体(报文中)
       conn.connect();
       DataOutputStream dos=new DataOutputStream(conn.getOutputStream());  //数据输出流
       dos.writeBytes(param);
       dos.flush();                    //真正塞进去
       dos.close();
       
       
       //获取服务器响应回来的信息
       int resultcode = conn.getResponseCode();
       if(resultcode == HttpURLConnection.HTTP_OK) {
           BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(),"UTF-8"));
           String line = null;
           while((line = reader.readLine())!=null) {
               System.out.println(line);
           }
       }
   }
}

image.png

服务器端(项目qihangkttest)IndexController.java映射方法,响应post请求。

//前端显示首页,输入 :   项目名/,项止名/inex.html,项目名/--都可访问到首面
@Controller
public class IndexController {
    @GetMapping(value= {"/index.html","/","index"})
   public String index() {
       return "index";
   }
    

    @RequestMapping(value="/postIndex.html",method=RequestMethod.POST)
    public String postIndex(Model model,HttpServletRequest request) throws UnsupportedEncodingException {
        request.setCharacterEncoding("UTF-8");
        model.addAttribute("name","post请求过来的值--"+request.getParameter("name"));
        System.out.println(request.getParameter("name"));
        return "index1";
    }
}

index1.jsp文件

image.png

测试:运行java项目的Demo5.java的main方法即可在控制台中显示:熊少文*
image.png

总结:
a: HttpURLConnection的connect()函数,实际上只是建立了一个与服务器的tcp连接,并没有实际发送http请求。无论是post还是get,http请求实际上直到HttpURLConnection的getInputStream()这个函数里面才正式发送出去。
b:在用POST方式发送URL请求时,URL请求参数的设定顺序是重中之重,对connection对象的一切配置(那一堆set函数)都必须要在connect()函数执行之前完成。而对outputStream的写操作,又必须要在inputStream的读操作之前。这些顺序实际上是由http请求的格式决定的。如果inputStream读操作在outputStream的写操作之前,会抛出例外:java.net.ProtocolException: Cannot write output after reading input.......
c:http请求实际上由两部分组成,
一个是http头,所有关于此次http请求的配置都在http头里面定义,
一个是正文content。
connect()函数会根据HttpURLConnection对象的配置值生成http头部信息,因此在调用connect函数之前,就必须把所有的配置准备好。

基于TCP协议的网络编程

利用 TCP 协议进行通信的两个应用程序是有主次之分的,
一个是服务器程序,一个是客户端程序,
两者的功能和编写方法不太一样,
其中 ServerSocket 类表示 Socket 服务器端,Socket 类表示 Socket 客户端。
基于TCP的套接字编程实现流程:
1.服务器端流程:
(1)创建套接字(ServerSocket)
(2)将套接字绑定到一个本地地址和端口上(bind)
(3)将套接字设定为监听模式,准备接受客户端请求(listen)
(4)阻塞等待客户端请求到来。当请求到来后,接受连接请求,返回一个新的对应于此客户端连接的套接字
sockClient(accept)(会等客户请求一直等)
(5)用返回的套接字sockClient和客户端进行通信(send/recv);
(6)返回,等待另一个客户端请求(accept)
(7)关闭套接字(close)
2.客户端流程:
(1) 创建套接字(socket)
(2) 向服务器发出连接请求(connect)
(3) 和服务器进行通信(send/recv)
(4) 关闭套接字(close)

image.png

ServerSocket 类
---用于在服务器上开一个端口,被动地等待数据(使用 accept() 方法)并建立连接进行数据交互。
服务器套接字一次可以与一个套接字连接,如果多台客户端同时提出连接请求,请求连接的客户端会被存入一个队列中,然后从中取出一个套接字与服务器新建的套接字连接起来。
---若请求连接大于最大容纳数,则多出的连接请求被拒绝;默认的队列大小是 50。
下面简单介绍一下 ServerSocket 的构造方法和常用方法。
ServerSocket 的构造方法如下所示。
ServerSocket():无参构造方法。
ServerSocket(int port):创建绑定到特定端口的服务器套接字。
ServerSocket(int port,int backlog):使用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口。
ServerSocket(int port,int backlog,InetAddress bindAddr):使用指定的端口、监听 backlog和要绑定到本地的 IP 地址创建服务器。
创建 ServerSocket 时可能会拋出 IOException 异常,所以要进行异常捕捉。

try{
       ServerSocket serverSocket=new ServerSocket(8111);
}catch(IOException e){
    e.printStackTrace();
}

ServerSocket 的常用方法
一,Server accept():监听并接收到此套接字的连接。
二,void bind(SocketAddress endpoint):将 ServerSocket 绑定到指定地址(IP 地址和端口号)。
三,void close():关闭此套接字。
四,InetAddress getInetAddress():返回此服务器套接字的本地地址。
五,int getLocalPort():返回此套接字监听的端口。
六,SocketAddress getLocalSocketAddress():返回此套接字绑定的端口的地址,如果尚未绑定则返回null。
七,int getReceiveBufferSize():获取此 ServerSocket 的 SO_RCVBUF 选项的值,该值是从ServerSocket 接收的套接字的建议缓冲区大小。
调用 accept() 方法会返回一个和客户端 Socket 对象相连接的 Socket 对象,服务器端的 Socket使用 getOutputStream() 方法获得的输出流将指向客户端 Socket 使用 getInputStream() 方法获得那个输入流。
同样,服务器端的 Socket使用的 getInputStream() 方法获得的输入流将指向客户端 Socket 使用的 getOutputStream() 方法获得的那个输出流。也就是说,当服务器向输出流写入信息时,客户端通过相应的输入流就能读取,反之同样如此。

Socket 类
---Socket 类表示通信双方中的客户端,用于呼叫远端机器上的一个端口,主动向服务器端发送数据(当连接建立后也能接收数据)。
Socket 的构造方法
Socket():无参构造方法。
Socket(InetAddress address,int port):创建一个流套接字并将其连接到指定 IP 地址的指定端口。
Soclcet(InetAddress address,int port,InetAddress localAddr,int localPort):创建一个套接字并将其连接到指定远程地址上的指定远程端口。
Socket(String host,int port):创建一个流套接字并将其连接到指定主机上的指定端口。
Socket(String host,int port,InetAddress localAddr,int localPort):创建一个套接字并将其连接到指定远程地址上的指定远程端口。
Socket 的常用方法
一,void bind(SocketAddress bindpoint):将套接字绑定到本地地址。
二,void close():关闭此套接字。
三,void connect(SocketAddress endpoint):将此套接字连接到服务器。
四,InetAddress getInetAddress():返回套接字的连接地址。
五,InetAddress getLocalAddress():获取套接字绑定的本地地址。
六,InputStream getInputStream():返回此套接字的字节输入流(不好手,般要转为字符流)。
七,OutputStream getOutputStream():返回此套接字的输出流。
八,SocketAddress getLocalSocketAddress():返回此套接字绑定的端点地址,如果尚未绑定则返回
null。
九,SocketAddress getRemoteSocketAddress():返回此套接字的连接的端点地址,如果尚未连接则返回 null。
十,int getLoacalPort():返回此套接字绑定的本地端口。
十一,int getPort():返回此套接字连接的远程端口。
例子:客户端与服务器端的简单通信
Server.java

public class Server {
    public static void main(String[] args) throws IOException {
        //根据流程第一步,创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(); //无参数,表示socket没有链接到任何设备上
        serverSocket.bind(new InetSocketAddress("127.0.0.1",40000));  //链接到本机,端口号40000
        //用一个循环来不断的接收客户端的链接
        while(true) {
            //接收客户端的链接请求,
            Socket clientSocket=serverSocket.accept();   //此方法会阻塞,直到客户端有请求为止,他会返回一个与连进来的客户端一一对应的socket
            //通过clientSocket对应的输入流来接收数据
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(),"GBK"));//InputStream该流是字节码流,很不好用,所以别建字符流
            //通过in获取客户端发来的信息
            String str = in.readLine();
            System.out.println("客户端发来的信息: "+str);
            
            
            //发送信息
            PrintStream ps = new PrintStream(clientSocket.getOutputStream(),true,"GBK");  //ture自动flush,'GBK'表示DOS用的中文码
            ps.println("我是服务器!");
            //serverSocket.close();   //不能关闭
            ps.close();
            clientSocket.close();
        }
    }

}

Client.java

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket();  //无参,默认连接本机
        socket.connect(new InetSocketAddress("127.0.0.1",40000));
        
        //发送数据
        PrintStream ps=new PrintStream(socket.getOutputStream(),true,"GBK");
        ps.println("我是客户端");
        
        //接收数据
        BufferedReader in =new BufferedReader(new InputStreamReader(socket.getInputStream(),"GBK"));
        String str=in.readLine();
        System.out.println("服务器端发送来的数据是: "+str);
        in.close();
        ps.close();
        socket.close();
    }

}

测试:先运行Server.java的main方法,再运行Client.java的main方法

image.png

注:为什么用GBK编码呢
---因为我们要用到CMD(DOS窗口)来测试网络代码,多个DOS窗口表示多个客户端。而DOS命令窗口的编码为GBK.

使用BIO实现网络通信

---前面的课中,其实已经粗略地实现了一个基于BIO的一个网络通信实例,但是,我们这章中的目标是实现一个基于命令行的能群聊的聊天室案例,针对这个目标,前面的例子也就只能是一个简单的客户端与服务器端的通信例子,很多细节的东西,需要我们进一步的完善!
---进一步思考,我们要做一个聊天室,既要实现在公共频道说的话,所有的客户端都看得见,知道是哪个说的,也要实现私聊的功能。应该怎么样进一步的完善,前面的简单例子!?
首先:暂时把服务器端发送给客户端的死信息去掉,需要在服务器端使用List来保存所有链接进来的客户端的Socket,要用多线程来处理每个链接上来的客户端。服务器端要把收到的所有客户端发送来的数据“广播”给其他的客户端。
----客户端也要升级,发送给服务器的信息,不能是死的,也要用多线程,主线程负责读取用户在键盘上输入的信息,子线程负责读取和显示服务器端发送过来的数据。
再次:实现私聊功能,也就是说一个客户端可以将信息发送给另一个指定的客户端。
实际上,所有客户端只与服务器端链接,客户端直接并没有互联,也就是说,当一个客户端发送信息后,
服务器端必须要判断信息到底是向所有用户发送,还是向指定用户发送。这些功能就要求,每个客户端必有有自己的特征属性!
这里更具体的细节:
1、客户端发送来的信息必须要有标记,让服务器能够判断信息是公聊信息,还是私聊信息,是哪个用户发的。
2、如果是私聊信息,客户端信息的目标客户端是哪个,服务器必须知道,才能转发到指定的客户端。
解决办法:在客户端发送的信息上动手脚,在信息前加前后缀,这些特殊前后缀字符
就是协议字符。
这些协议字符,有的标记公聊还是私聊,有的标记客户端身份......
服务器端第二次升级:定义一个接口规定协议字符串,定义一个数据结构保存聊天室用户和对应Socket关联的输出流直接的映射关系,更新Server类添加异常捕获,更新ServerThread类添加判断信息类型。
客户端第二次升级:更新Client类添加用户名的输入,也就是登录功能,更新获取键盘信息并发送的功能,要加上协议字符前后缀,更新ClientThread类关闭流。
解决端口号被占用的情况:
一,找到占用端口的进程PID.

netstat -ano | findstr 8080

二,杀死进程

taskkill -pid xxxx -f

例1:简易聊天系统---没有私聊功能。
大概:利用多线程接收服务器或客户端信息再显示,分别对应的代码:Server.java,ServerThread.java,Client.java,ClientThread.java四个文件。服务端我们在IDEA中启动不关,再把两个客户端文件复制放到另一个地方。然后:javac Client.java编译文件,它会把调用的类也编译了。其次:java Client 多个客户端都可以输入java Client,现在不用再编译了。”

image.png

Server.java,ServerThread.java

public class Server {
       //定义list来保存所有链接进来的Socket(客户端),需要包装线程安全的list集合, public static List synchronizedList(List l);
    public static List socketlist = Collections.synchronizedList(new ArrayList<>());
    public static void main(String[] args) throws IOException {
           //根据流程第一步,创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(); //无参数,表示socket没有链接到任何设备上
        serverSocket.bind(new InetSocketAddress("127.0.0.1",40000));  //链接到本机,端口号40000
           //用一个循环来不断的接收客户端的链接
        while(true) {
              //接收客户端的链接请求,
            Socket clientSocket=serverSocket.accept();   //此方法会阻塞,直到客户端有请求为止,他会返回一个与连进来的客户端一一对应的socket
            Server.socketlist.add(clientSocket) ;       //不能像Server_dijide.java那样,拿一个,用一个,丢一个 ,要多线程接收多客户端
               //多客户端的情况下,我们要所有的客户端都保存起来

               //不能像单线程一样,去链接客户端,效率太低,我们要用多线程来处理,每来一个客户端,就分配一条线程去接收。
            new Thread(new ServerThread(clientSocket)).start();
        }
    }

}

ServerThread.java


/**
 * 线程类,是用来处理与密户端直接的数据交互的
 * 肯定就需要,与客户端对应的那个套接字
 */
public class ServerThread implements  Runnable{
    private Socket socket = null;
    //private List socketList = null;
    private BufferedReader br =null;   //字符流
    public ServerThread(Socket socket) throws IOException {
        this.socket = socket;
        //this.socketList = socketList;
        br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"GBK"));
    }
    @Override
    public void run() {
         String content = null;   //收到的消息,也是要广播出去的消息
        //用循环不断接受,不断的广播
        while((content=readFromClient())!=null){
            //广播
            for(Socket s:Server.socketlist){
                try{     //这是线程类run方法,等下线程调用不能抛出异常,只能捕获之
                    PrintStream ps =new PrintStream(s.getOutputStream(),true,"GBK");
                    ps.println(content);   //广发出去(所有的客户端)
                }catch(IOException e){
                    e.printStackTrace();
                }
            }

        }

    }
    //单独地定义服务器接收客户端信息的方法
    private String readFromClient(){
          try{
              return br.readLine();  //读到了信息就返回,但有可能读不到,对应客户端的Socket可以会关闭
          }catch (Exception e){
              e.printStackTrace();
              //读不到信息,表示客户端已经关了,把关掉的客户端从我们的list集合删除
              Server.socketlist.remove(socket);
          }
          return null;
    }
}

Client.java

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket();  //无参,默认连接本机
        socket.connect(new InetSocketAddress("127.0.0.1",40000));

        //开启一个子线路,负责获取和显示服务器哪里发送来的数据信息
        new Thread(new ClientThread(socket)).start();
        //发送数据(这次发送的数据从键盘上输入System.in输入流)
        PrintStream ps=new PrintStream(socket.getOutputStream(),true,"GBK");

        BufferedReader br =new BufferedReader(new InputStreamReader(System.in,"GBK"));
        String line =null;
        //System.out.println("服务器端发送来的数据是: "+str);
        while((line=br.readLine())!=null){
            ps.println(line);
        }
        br.close();
        ps.close();
        socket.close();
    }
}

ClientThread.java

/**
 * 负责获取和显示从服务器哪里发旁顾来的信息
 */
public class ClientThread implements Runnable {
    private Socket socket = null;
    private BufferedReader in = null;

    public ClientThread(Socket socket) throws IOException {
        this.socket = socket;
        in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "GBK"));
    }

    //构造方法,初始化socke和字符流
    @Override
    public void run() {
        try {  //正式的从服务器获取数据,显示从服务器发送来的信息
            String content = null;
            //循环,不断地获取
            while ((content = in.readLine()) != null) {
                System.out.println(content);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

image.png

image.png

注意:由于我们没有写JAVA GUI界面,所以此处只能用WINDOWS的DOS窗口测试,由于他默认编码为GBK,所以我们复制Clinet.java,ClientThread.java保存为ANSI(GBK)文件类刑。

image.png

image.png

image.png

image.png
image.png

例2:简易聊天系统---有私聊功能。

image.png

四个客户端与及相关文件拷到一个目录下。


image.png

---再打开三个DOS窗口,先javac Client.java再java Client运行客户端。分别输入三个用户名:xiong,xu,huang.当xu想向xiong私聊时,可以这样发送信息-----//xiong:你好!,这样只有xiong看到,huang看不到这信息。

image.png

它们相关代码如下:
ChatRoomMap.java 封装键值对,一个是用户名,一个时发送流。

/**
 * 我们新定义的一个数据结构,用来保存用户和socket对应的输入流之间的映射关系
 * @param :代表客户端的用户名
 * @param :是一个输出流PrintStream
 */

public class ChatRoomMap {
    //本质上保存数据的是一个特珠的HashMap,我们定义过的,线程安全的
    public Map map = Collections.synchronizedMap(new HashMap<>());

    //可以根据v来删除指定的项目
    public synchronized void removeByValue(Object value){
        for(Object key:map.keySet()){
            if(map.get(key) == value){
                map.remove(key);
                break;
            }
        }
    }
    //获取所有的value组合成的set集合
    public synchronized Set getValueSet(){
        //将map中的value添加到res集合里
        Set res = new HashSet<>();
        for(Object key:map.keySet()){
            res.add(map.get(key));
        }
        return res;
    }

    //根据value值来找到key
    public synchronized K getKeyByValue(V val) {
        for(K key:map.keySet()){
        if (map.get(key) == val || map.get(key).equals(val)) {
            return key;
        }
     }
    return null;
    }
    //实现添加数据到ChatRoomMap中,些数据结构规定value不能重复
    public synchronized  V put(K key,V value){
        //遍历所有的value值判断有重得没有
        for(V val:getValueSet()){
            if(val.equals(value) && val.hashCode() == value.hashCode()){
                throw new RuntimeException("Map实例中不充许有重复的value值!");
            }
        }
        return map.put(key,value);
    }

}

ChatRoomProtocol.java ---协议字符。

/**
 * 该接口不定义方法,只是定义一些聊天室的协议,规则:私聊前后缀,登录成功前后缀等符号
 */
public interface ChatRoomProtocol {
    //定义一个协议字符串的长度
    int PROTOCOL_LEN = 2;
    String PUBLICMSG_ROUND = "】⊿"; //公聊消息的前后缀
    String USER_ROUND = "﹩○"; //用户名称的前后缀
    String LOGIN_SUCCESS = "1"; //登录成功的前后缀
    String NAME_REP = "-1"; //后面在客户端发送信息前,要求输入用户名,重复了返回这个标记
    String PRIVATEMSG_ROUND = "﹩&"; //私聊信息的前后缀
    String SPLIT_SIGN = "?※"; //信息的分割标记
}

Server.java

public class Server {
    public static final int SERVER_PORT = 40000;     //常量

       //把原Server_zhongji定的list换成我们自定义的新数据结构,来保存链接进来的所有客户端,前面保存是Socket对象
       //现在我们保存的是代表客户端的用户名称和对应的socket关联的输出流
    public static ChatRoomMap clients = new ChatRoomMap();
        //绑定ip地址和端口的启动服务器的代码封装在init方法中
    public void init() {
        try {
            //根据流程第一步,创建ServerSocket
            ServerSocket serverSocket = new ServerSocket(); //无参数,表示socket没有链接到任何设备上
            serverSocket.bind(new InetSocketAddress("127.0.0.1", 40000));  //链接到本机,端口号40000
            //用一个循环来不断的接收客户端的链接
            while (true) {
                //接收客户端的链接请求,
                Socket clientSocket = serverSocket.accept();   //此方法会阻塞,直到客户端有请求为止,他会返回一个与连进来的客户端一一对应的socket

                //不能像单线程一样,去链接客户端,效率太低,我们要用多线程来处理,每来一个客户端,就分配一条线程去接收。
                new Thread(new ServerThread(clientSocket)).start();
            }
        } catch (Exception e) {
            System.out.println("服务器启动失败,可能是端口号: "+SERVER_PORT+"被点用!");
        }
    }
    public static void main(String[] args){
               Server server= new Server();
               server.init();
        }


}

ServerThread.java

/**
 * 线程类,是用来处理与密户端直接的数据交互的
 * 肯定就需要,与客户端对应的那个套接字
 */
public class ServerThread implements  Runnable{
    private Socket socket = null;
    private BufferedReader br =null;   //字符流
    private PrintStream ps = null;
    public ServerThread(Socket socket) throws IOException {
        this.socket = socket;

    }
    @Override
    public void run() {
        try{
             //获取客户端对应的输入流
            br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"GBK"));

            //再拿到客户端对应的输出流,发送信息
            ps= new PrintStream(socket.getOutputStream(),true,"GBK");

            //先通过br读取数据
            String lines = null;

            while((lines = br.readLine())!=null){
                //先读取客户端发送过来的用户名(客户端发送信息时加用户名),用到协议字符了。协议规定,客户端发送来的信息,必须是USER_ROUND作为信息前后缀
                if(lines.startsWith(ChatRoomProtocol.USER_ROUND) &&  lines.endsWith(ChatRoomProtocol.USER_ROUND)){

                      //前后缀都为USER_ROUND即是有用户我的信息,再拿到真正的用户名
                    String userName = getRealMsg(lines);

                    //判断用户名重复吗
                    if(Server.clients.map.containsKey(userName)){
                        System.out.println("用户名重复了!");    //发送到空制台
                        ps.print(ChatRoomProtocol.NAME_REP);  //发送客户  NAMME_
                    }else{
                        System.out.println("用户登陆成功!");
                        ps.println(ChatRoomProtocol.LOGIN_SUCCESS);  //LOGIN_SUCCESS=1
                        Server.clients.put(userName,ps);
                    }

                }else if(lines.startsWith(ChatRoomProtocol.PRIVATEMSG_ROUND) && lines.endsWith(ChatRoomProtocol.PRIVATEMSG_ROUND)){
                    //客户端发送来的信息是私聊,拿到信息
                    String userAndMsg = getRealMsg(lines);
                    //上面的信息是用ChatRoomProtocol.SPLIT_STGN来隔开的
                    String targetUser = userAndMsg.split(ChatRoomProtocol.SPLIT_SIGN)[0];
                    String privatemsg = userAndMsg.split(ChatRoomProtocol.SPLIT_SIGN)[1];

                    //服务器就可以转发指定的用户了
                    Server.clients.map.get(targetUser).println(Server.clients.getKeyByValue(ps)+"私聊说:"+privatemsg);

                }else{   //最后一种:公聊
                    String publicmsg = getRealMsg(lines); //拿到公聊信息
                    //广播
                    for(PrintStream clientPs:Server.clients.getValueSet()){
                        clientPs.println(Server.clients.getKeyByValue(ps)+" 说:"+publicmsg);
                    }

                }

            }
        }catch (Exception e){
              //在上面的过程中,发生异常,标记服务器和客端的Socket发送数据交换异常
            //这个客户端可能已经关闭了,这人客户端应该人我的clients集合中删除
            Server.clients.removeByValue(ps);
            System.out.println(Server.clients.map.size());

            //关闭io,网络
            try{
                if(br!=null)
                    br.close();
                if(ps!=null)
                    ps.close();
                if(socket!=null)
                    socket.close();
            }catch(Exception ex){
                ex.printStackTrace();
            }
        }


    }


    //去除协议字符的方法,PROTOCOL_LEN=2
    private String getRealMsg(String lines){
        return lines.substring(ChatRoomProtocol.PROTOCOL_LEN,lines.length()-ChatRoomProtocol.PROTOCOL_LEN);
    }
}

Client.java

public class Client {
    private static final  int SERVER_PORT=40000;
    private Socket socket;
    private PrintStream ps = null;
    private BufferedReader inServer = null;   //服务器上输入流
    private BufferedReader inKey = null;      //客户端上输入流
   
    /**
     * 客户端链接服务器功能
     */
    public void init() {
        try {
            //首先键盘的输入流初始化
            inKey = new BufferedReader(new InputStreamReader(System.in,"GBK"));
            
            //链接服务器
            socket= new Socket("127.0.0.1",SERVER_PORT);
            
            //获取socket对应的输入输出流
            ps = new PrintStream(socket.getOutputStream(),true,"GBK");
            inServer = new BufferedReader(new InputStreamReader(socket.getInputStream(),"GBK"));
            
            //用一个循环来进行服务器的登录,用GUI对话框弹出框
            String tip ="";
            while(true) {
              String userName = JOptionPane.showInputDialog(tip+"请输入用户名: ");
              
               //就用用户输入用户名发送给服务器
              ps.println(ChatRoomProtocol.USER_ROUND+userName+ChatRoomProtocol.USER_ROUND);
              
              //发送后,紧接着获取服务器的响应
              String res=inServer.readLine();
                   //用户名重复了,返回‘-1’
              if(res.equals(ChatRoomProtocol.NAME_REP)) {
                  tip="用户名重复,请重新";
                  continue;
              }
              if(res.equals(ChatRoomProtocol.LOGIN_SUCCESS)) {
                  break;
              }
            }
            
        }catch(UnknownHostException el) {
            System.out.println("找不到服务器,请确认服务器是否启动!");
            closeRes();
            System.exit(1);//退出系统
        }catch(IOException e2) {
            System.out.print("网络异常,请确定网络是否链接");
            closeRes();
            System.exit(1);
        }
        
        //启动线程,获取服务器的响应信息,在控制台上显示
        new Thread(new ClientThread(inServer)).start();
    }
    /**
     * 客户端获取键盘上输入的信息并发送给服务器功能
     */
    private void readAndSend() {
        try {
            String line =null;
            while((line=inKey.readLine())!=null) {
                //对line的内容进行判断,发送的是私聊信息, 还是公聊信息
                //规定:发送的信息如果有冒号,并且是以‘//’开头,表示你发送的供应商息是私聊信息.例:‘ //徐会凤:您好!’
                if(line.indexOf(":")>0 && line.startsWith("//")) {
                    line = line.substring(2);            //去掉'//'
                    ps.println(ChatRoomProtocol.PRIVATEMSG_ROUND+
                            line.split(":")[0]+ChatRoomProtocol.SPLIT_SIGN+
                            line.split(":")[1]+ChatRoomProtocol.PRIVATEMSG_ROUND);  //发送私聊信息加上协义字符PRIVATEMSG_ROUND
                }else {//不是私有就是公有的了,只要广播line就可以了
                     ps.println(ChatRoomProtocol.PUBLICMSG_ROUND+line+ChatRoomProtocol.PUBLICMSG_ROUND);
                    
                }
            }
        }catch(IOException e) {
            System.out.println("网络通信异常,请检查网络是否通畅!");
             closeRes();
             System.exit(1);
            
        }
        
    }
    
    
    private void closeRes() {
        try {
            if(inKey!=null) inKey.close();
            if(inServer!=null) inServer.close();
            if(ps!=null) ps.close();
            if(socket!=null) socket.close();
        }catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws IOException {
        Client client = new Client();
        client.init();
        client.readAndSend();   //不断获取键盘数据,发送出去
        

    }
}

ClientThread.java

/**
 * 负责获取和显示从服务器哪里发旁顾来的信息
 */
public class ClientThread implements Runnable {
    private BufferedReader in = null;

    public ClientThread(BufferedReader in ) {
        this.in = in ;
    }

    //构造方法,初始化socke和字符流
    @Override
    public void run() {
        try {  //正式的从服务器获取数据,显示从服务器发送来的信息
            String content = null;
            //循环,不断地获取
            while ((content = in.readLine()) != null) {
                System.out.println(content);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(in!=null) in.close();
            }catch(Exception e) {
                e.printStackTrace();
            }
        }
    }
}

你可能感兴趣的:(计算机网络)