一道socket题目引发的思考

这里写图片描述

一、题目及分析

1、题目

客户端通过键盘录入用户名。
服务端对这个用户名进行校验。
如果该用户存在,在服务端显示xxx,已登陆。
并在客户端显示 xxx,欢迎光临。
如果该用户不存在,在服务端显示xxx,尝试登陆。
并在客户端显示 xxx,该用户不存在。
最多就登录三次。

2、【错误】答案1

客户端:
package com.ht.zuoye12;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

import org.omg.Messaging.SyncScopeHelper;

public class ZyTcpSend {

    public static void main(String[] args) throws IOException {
        //定义一个count来记录登陆的次数
        int count = 0;
        //键盘输入,并传送用户名给服务器端
        BufferedReader bufferedReader = null;
        Socket s = new Socket(InetAddress.getByName("127.0.0.1"), 18888);
        PrintWriter pwWriter = new PrintWriter(s.getOutputStream());
        InputStream in = s.getInputStream();
        while(count < 3) {
            bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            System.out.print("请输入登陆名:");
            String line = bufferedReader.readLine();
            pwWriter.print(line);
            pwWriter.flush();

            //客户端收到服务器发送过来的验证反馈并显示

            byte[] b = new byte[1024];
            int len = in.read(b);
            String feedback = new String(b, 0, len);
            System.out.println(feedback);
            if(feedback.equals(line + ",欢迎光临")) {
                break;
            }
            if(count == 2) {
                System.out.println("登陆超过3次");
            }
            count ++;
        }

//流和socket都未关闭      
    }

}
服务器端:
package com.ht.zuoye12;

/*
 * 时间:150412
 * 功能:验证用户名的服务器端
 */
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class ZyTcpRec {

    public static void main(String[] args) throws IOException {
        System.out.println("服务器启动...");
        // 读取客户端发过来的用户名
        ServerSocket serverSocket = new ServerSocket(18888);
        Socket s = serverSocket.accept();
        DataInputStream in = new DataInputStream(s.getInputStream());
        PrintWriter pWriter = new PrintWriter(s.getOutputStream());

        while (true) {          
            byte[] b = new byte[1024];  
            int len = in.read(b);
            String name = new String(b, 0, len);
            System.out.println(name);

            if (name.equals("admin")) {
                System.out.println(name + ",已登录");
                pWriter.print(name + ",欢迎光临");
                pWriter.flush();
            }
            else {
                System.out.println(name + ",尝试登陆");
                pWriter.print(name + ",该用户不存在");
                pWriter.flush();
            }
        }

    }

}
错误原因分析:Connection reset异常

报出:java.net.SocketException: Connection reset异常。
分析:
  如果第一次就输入正确,即admin,服务器端第一次读完数据并输出后,服务器端也把反馈发送给客户端了,客户端break后向下运行,直至结束。而此时,服务器端判断是true,第二次回来继续read,(如果客户端没有退出或者输入流也没有关闭或者socket没有关闭,即客户端的红色一直在亮着)按常理说是阻塞在这里的,但是现在是客户端已经停止运行了。但是并没有通知服务器端我的输入流结束啦。所以服务器端在那里等待着读。问题正好是下面描述的第二种情况:(客户端退出时并未关闭连接)

java.net.SocketException: (Connection reset或者Connect reset by peer:Socket write error)。该异常在客户端和服务器端均有可能发生,引起该异常的原因有两个,第一个就是如果一端的Socket被关闭(或主动关闭或者因为异常退出而引起的关闭),另一端仍发送数据,发送的第一个数据包引发该异常(Connect reset by peer)。另一个是一端退出,但退出时并未关闭该连接,另一端如果在从连接中读数据则抛出该异常(Connection reset)。简单的说就是在连接断开后的读和写操作引起的。

3、【错误】改进答案2(客户端关闭socket)

客户端:

这里只关socket就可以,因为socket关闭,相当于输入输出流都关上了。

增加部分:       
bufferedReader.close();
pwWriter.close();
in.close();
s.close();
服务器端:(不变)
错误原因分析:StringIndexOutOfBoundsException

  java.lang.StringIndexOutOfBoundsException: String index out of range: -1
  为什么会这样呢?
  如果第一次就输入正确,即admin,服务器端第一次读完数据并输出后,服务器端也把反馈发送给客户端了,客户端break后向下运行,把输入流、输出流、socket都关闭了。因为服务器第二次循环回来之后,在那里阻塞了,但是因为输入流已经关上了,所以满足read方法解除阻塞的条件(读到流的末尾),所以read其实是读到了流的末尾了,就是说len=-1,然后向下执行,然后String name = new String(b, 0, len);,自然会出现下标越界的问题。

4、【错误】改进答案3

客户端:(跟答案2相同,不变)
服务器端:(把读取一次服务器端,改为循环读)
while (true) {

            byte[] b = new byte[1024];
            int len = 0;
            String name = null;
//修改的地方
            while ((len = in.read(b)) != -1) {
                name = new String(b, 0, len);
                System.out.println(name);
            }

            if (name.equals("admin")) {
                System.out.println(name + ",已登录");
                pWriter.print(name + ",欢迎光临");
                pWriter.flush();
            }
            else {
                System.out.println(name + ",尝试登陆");
                pWriter.print(name + ",该用户不存在");
                pWriter.flush();
            }
        }
错误原因分析:陷入互等中,无法执行下去

  当客户端第一次输入数据admin时,服务器端read到了,并且输出了,你希望服务器端接下来去执行下面代码,事实上,这个时候你的输入流也没有关闭,然后也没有发生异常,所以服务器端会在while循环里,一直read,一直等,等着你的输入,所以不会按照你的预想去执行下面的代码。客户端这边呢,因为收不到服务器端的反馈,也在那里一直读服务器端的反馈。大家陷入互等中。问题的根本就是,服务器端何时是输入流的末尾。
  A进程与B进程通过Socket通信.假定A输出数据,B读入数据.A如何告诉B所有数据已经输出完毕?

方式1:A与B交换的是字符流,且一行一行的读写.可事先约定以一个特殊标志作为结束标志,如以”bye”作为结束标志.当A向B发送一行字符串”bye”时,B读到这一行数据时,则停止读数据。
方式2:进程A先发送消息,告诉B所发送正文的长度.->再发送正文.->B先获知A发送的正文长度->接下来只要读取完该长度的字符或者字节,就停止读数据.
方式3:A发完所有数据后,关闭Socket->B读取A发送的所有数据后,B的输入流的read方法返回-1,B知道A数据输完了。
方式4:调用Socket半关闭方法shutdownInput():关闭输入流与 shutdownOutput():关闭输出流。
B读取数据时,如果A的输出流已经关闭,那么B读入所有数据后,就会读到输入流的末尾.
注意:先后调用Socket的shutdonwInput和shutdownOutput方法.仅仅是关闭了输入流和输出流,并不等价Socket close调用,当通信结束后,依然要调用Socket的close方法.只有该方法才会释放Socket占用的资源.如占用的本能地端口等。
方式5:发生了异常,通信一方运行终止了。

5、【错误】改进答案4(在客户端使用shutdown方法)

客户端:(在客户端使用shutdown方法)
就是多了一句s.shutdownOutput();
while(count < 3) {
            bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            System.out.print("请输入登陆名:");
            String line = bufferedReader.readLine();
            pwWriter.print(line);
            pwWriter.flush();

//新增加的内容
            s.shutdownOutput();

            //客户端收到服务器发送过来的验证反馈并显示

            byte[] b = new byte[1024];
            int len = in.read(b);
            String feedback = new String(b, 0, len);
            System.out.println(feedback);
            if(feedback.equals(line + ",欢迎光临")) {
                break;
            }
            if(count == 2) {
                System.out.println("登陆超过3次");
            }
            count ++;
        }
服务器端:(与答案2相同,不变)
错误分析:可以打破等待,但出现异常

这里写图片描述
  客户端第一次输入111,然后输出流关闭(注意分清楚要关闭的是哪个流,服务器端的输入相对的是客户端的输出),服务器端接收到111,并且read方法也收到了客户端输出流结束的消息,即read方法返回-1.①然后服务器端就可以执行下面的反馈代码了。
  ②客户端收到了反馈结果,并打印了出来。(与此同时,或者之前或者之后,③服务器端判断是true,然后又执行了循环,但是name=null,所以到equals方法的时候会抛出空指针异常。注:这里涉及线程的知识,①和②是有顺序的,服务器端和客户端是两个线程,但是①必须先于②执行,但是②和③的执行顺序就没有先后关系了)。
  而客户端打印出反馈结果后,因为count=1,所以又进入循环,提示我们输入数据,我们输入了222,到写入输出流的时候,这个时候就会报异常,因为我们在第一次循环的时候已经把socket的输出流关闭掉了。

6、【正确】改进答案5

客户端:(去掉shutdown)
服务器端:(把反馈代码放到read循环里面)
while (true) {

            byte[] b = new byte[1024];
            int len = 0;
            String name = null;
            while ((len = in.read(b)) != -1) {
                name = new String(b, 0, len);
                System.out.println(name);

                if (name.equals("admin")) {
                    System.out.println(name + ",已登录");
                    pWriter.print(name + ",欢迎光临");
                    pWriter.flush();
                }
                else {
                    System.out.println(name + ",尝试登陆");
                    pWriter.print(name + ",该用户不存在");
                    pWriter.flush();
                }
            }


        }

7、【正确】改进答案6(使用available方法,不建议使用)

客户端:(与改进5一样)
服务器端:
while (true) {

            byte[] b = new byte[1024];

            int len;
            String name = null;

//改动的地方
            while (in.available() > 0) {
                System.out.println(in.available());
                len = in.read(b);
                name = new String(b, 0, len);

                if (name.equals("admin")) {
                    System.out.println(name + ",已登录");
                    pWriter.print(name + ",欢迎光临");
                    pWriter.flush();
                }
                else {
                    System.out.println(name + ",尝试登陆");
                    pWriter.print(name + ",该用户不存在");
                    pWriter.flush();
                }
            }

二、知识延伸

1、available方法分析

  inputstream.available()方法返回的值是该inputstream在不被阻塞的情况下一次可以读取到的数据长度。如果数据还没有传输过来,那么这个inputstream势必会被阻塞,从而导致inputstream.available返回0。这个函数在文件操作时常用,但是涉及网络编程不建议使用,因为网络是不稳定的,也就是说网络下载时,read()方法是阻塞的,说明这时我们用inputStream.available()获取不到文件的总大小。

更多available方法的资料参考:
http://hold-on.iteye.com/blog/1017449
http://blog.csdn.net/lcfeng1982/article/details/7332723
http://jiangzhengjun.iteye.com/blog/509900

  Inputstream是抽象类,他的流源代码中的available方法是返回的0,要求子类重写这个方法。
  我们看一下socket的getInputstream方法:为啥可以呢,明明任何时候都是返回0,而socket也没有重写这个方法呀。。。

说明:socket.getInputStream() ,获取的实际是SocketInputStream(InputStream的实现类), SocketInputStream集成PlainSocketImpl, PlainSocketImpl有availabe()的具体实现

2、客户端socket关闭,会不会影响服务器端

  涉及到计算机网络的知识
  TCP建立连接以后,双方就是对等的。不论是哪一方,只要正常close(socket_handle),那么 TCP 底层软件都会向对端发送一个 FIN 包。
  FIN 包到达对方机器之后,对方机器的 TCP 软件会向应用层程序传递一个 EOF 字符,同时自动进入断开连接流程(要来回协商几次,但这些都是自动的、不可控的)。什么是 EOF 字符?它其实什么也不是,只是一个标记,上层应用程序如果这时读 socket 句柄的话,就会读到 EOF,也就是说,此时 socket 句柄看起来里面有数据,但是读不出来,因此 select 返回可读(非阻塞模式下)read 不会阻塞(阻塞模式下)但是 read 的返回值却是 0。
  如果此时不是读操作而是写操作,并且此时 socket 已经断开连接,那么 write 函数会返回 -1 且置 errno 为 EPIPE(如果忽略了 SIGPIPE 信号的话)或者引发 SIGPIPE 信号(如果没忽略的话)
  因此,客户端close后,服务器端是可以收到的,即服务器端的输入流就不会阻塞了。

  这里介绍了客户端关闭对服务器端的影响以及服务器端关闭对客户端的影响。
http://blog.csdn.net/hunkcai/article/details/5803651

3、阻塞式方法的说明

read() : 从输入流中读取数据的下一个字节,返回0到255范围内的int字节值。如果因为已经到达流末尾而没有可用的字节,则返回-1。在输入数据可用、检测到流末尾或者抛出异常前,此方法一直阻塞。
read(byte[] b) : 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。以整数形式返回实际读取的字节数。在输入数据可用、检测到文件末尾或者抛出异常前,此方法一直阻塞。

三、关于技术学习的观念

1、在学习的过程中要期待遇到问题。因为问题才会带来成长,可以用解决问题的个数和代码行数来横向自己某一方面技术的成长。
2、不只可以看源代码,还可以看api,养成习惯。
3、学会看库中源码的能力,一切问题都归于验证,看源码或者自己写代码验证。
4、程序出错,百度问题答案是一个技术黑洞,很容易被各种纷繁的资料牵着鼻子走,最好的方式是自己用导图组织,或者把自己的思考写下来,针对性的去查找问题答案。
5、解决问题的另一种方法,看源代码,ctrl+鼠标,直接就到这个方法的源代码了,然后很多的地方你就可以理解了。
6、遇到问题要会分析问题,自己先思考,可能有多个思考的点,然后百度,针对每个点进行验证。不要只看百度不写程序验证,也要根据百度的答案根据他的程序使用白板分析,或者自己把代码抄下来验证执行。有的时候可能网上也没有答案,那就自己根据自己的分析思路,自己对每一个思路写程序进行验证。自己思考很重要。
7、不要急躁。安静持续的投入其中,最近在考虑,程序员整天在这些琐碎的细节和bug里,何时才可以改变世界呢,做出可以让人们生活变得更加美好的产品呢?答案就是,不要急躁,安静持续的投入,在能力还很弱小的时候多积累。
8、遇到问题要开心,不要心存挫败感。大牛都是一个一个问题累计起来的。你很弱,就要多积累。
9、不仅要知其然,还要知其所以然。刘未鹏前辈讲的。

四、更多资料

http://haohaoxuexi.iteye.com/blog/1979837
该博客非常详细,跟着这篇博客的例子练习下来,对socket的理解会更深一层。

Java socket中关闭IO流后,发生什么事?(以关闭输出流为例)
http://blog.csdn.net/justoneroad/article/details/6962567

Socket部分常见的异常
http://blog.csdn.net/allanking666/article/details/5020864

Java网络编程精解笔记,博主的博客有很多深刻的读书笔记,建议关注
http://www.blogjava.net/landon/archive/2013/07/02/401137.html

怎样理解阻塞非阻塞与同步异步的区别?
http://www.zhihu.com/question/19732473
Java并发性和多线程
http://ifeve.com/java-concurrency-thread-directory/

JAVA多线程和并发基础面试问答
http://blog.jobbole.com/76308/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值