一道socket题目引发的思考

题目及分析

题目

客户端通过键盘录入用户名。

服务端对这个用户名进行校验。

如果该用户存在,在服务端显示xxx,已登陆。

并在客户端显示 xxx,欢迎光临。

如果该用户不存在,在服务端显示xxx,尝试登陆。

并在客户端显示 xxx,该用户不存在。

最多就登录三次。

【错误】答案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)。简单的说就是在连接断开后的读和写操作引起的。

【错误】改进答案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);,自然会出现下标越界的问题。

【错误】改进答案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:发生了异常,通信一方运行终止了。

【错误】改进答案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的输出流关闭掉了。

【正确】改进答案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();
                }
            }

        }

【正确】改进答案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();
                }
            }

知识延伸

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关闭,会不会影响服务器端

??涉及到计算机网络的知识

??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

阻塞式方法的说明

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/

时间: 2024-11-08 20:17:02

一道socket题目引发的思考的相关文章

一道笔试题目引发的思考

题目 下面说法错误的是(C) A.static成员函数没有this指针. B.static成员函数不能直接访问非static成员. C.static数据成员不能在类的定义体中初始化. D.static数据成员独立于类的任意对象而存在,不是该类类型对象的组成部分. 分析 A选项 静态成员函数由于不是与任何的对象相联系,因此它不具有this指针.从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数. B选项 //如下的类定义,下面这样stati

黑马程序员---Objective-C基础学习---一道课后习题引发的思考

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 一道课后习题引发的思考 /* 需求:设计一个类Point2D,用来表示二维平面中某个点 1> 属性 * double x * double y 2> 方法 * 属性相应的set和get方法 * 设计一个对象方法同时设置x和y * 设计一个对象方法计算跟其他点的距离 * 设计一个类方法计算两个点之间的距离 3> 提示 * C语言的math.h中有个函数:double pow(double

一道CTF题引发的思考-MySQL的几个特性

0x01   背景 前天在做一道CTF题目时一道盲注题,其实盲注也有可能可以回显数据的,如使用DNS或者HTTP等日志来快速的获取数据,MYSQL可以利用LOAD_FILE()函数读取数据,并向远程DNS主机发送数据信息,此时DNS日志文件中就会有盲注语句的查询结果.这里不做这部分的讨论,只是说下有这种方法,在这道题目中我是使用常规的盲注的方式获取数据的.其中遇到有以下几个问题: 过滤规则的判断与绕过 MySQL的一些少有人总结的特性 手动盲注的繁琐低效 这题确实让我思考了很多,当然还有一些特性

一道原生js题目引发的思考(鼠标停留区块计时)

我瞎逛个啥论坛,发现了一个题目,于是本着练手的心态就开始写起来了,于是各种问题接踵而至,收获不小. 题目是这样的: 刚看上去,没什么特别,心里想了,我就用mouseover和mouseout事件,然后绑个定时器不就行了嘛~.......于是还没开始写呢,就被问到了,那mouseover和mouseenter这两个事件有什么区别的?为什么不用mouseenter呢? 然后我就仔细想了下mouseover和mouseenter之间的区别,下面是书上列出的定义: ->mouseenter:在鼠标光标从

(转)c++类的成员函数存储方式(是否属于类的对象)---一道面试题引发的思考

昨天去面试一家公司,面试题中有一个题,自己没弄清楚,先记录如下: class D { public: void printA() { cout<<"printA"<<endl; } virtual void printB() { cout<<"printB"<<endl; } }; main函数调用: D *d=NULL; d->printA(); d->printB(); 输出结果是? 当时想的是对象d直

一道CTF题引发的思考-MySQL的几个特性(续)

0x00 背景 这两天处于转牛角尖的状态,非常不好.但是上一篇的中提到的问题总算是总结了些东西. 传送门:疑问点0x02(4) 0x01 测试过程 (1)测试环境情况:创建了如下测试表test, mysql> select * from test;+---------+-------+-----------------------------------------+| user_id   | user  | password    |+---------+-------+-----------

【C语言入门】由“换硬币”题目引发的思考

[Pt.I]问题概述: 小明手中有硬币,小红手中有若干张10元的纸币.已知 1 角硬币厚 1.8mm,5 角硬币厚 1.5mm,1 元硬币厚 2.0mm .小红拿出若干张10元的纸币,小明要将 1 角的硬币放成一摞,将 5 角的硬币放成一摞,将 1 元的硬币放成一摞,如果 3 摞硬币一样高,且三摞硬币的金额之和正好等于小红要求的面值,则双方交换,否则没有办法交换. 输入: 小红希望交换几张10元的纸币 输出: 1 角的数量,5 角的数量,1元的数量 <错误程序> #include<std

一个截取字符串函数引发的思考

背景 前些天,遇到这样一个问题,问题的内容如下: 要求编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串.但是要保证汉字不被截半个,如"我ABC", 4,截取后的效果应该为"我AB",输入"我ABC汉DEF", 6,应该输出为"我ABC",而不是"我ABC+汉的半个". 问题 刚看到这个问题的时候,以为还是很简单的,但写出来之后,发现并不是想要的效果.回想一下当时的思路,就发现刚开

曲演杂坛--一条DELETE引发的思考

原文:曲演杂坛--一条DELETE引发的思考 场景介绍: 我们有一张表,专门用来生成自增ID供业务使用,表结构如下: CREATE TABLE TB001 ( ID INT IDENTITY(1,1) PRIMARY KEY, DT DATETIME ) 每次业务想要获取一个新ID,就执行以下SQL: INSERT INTO TB001(DT) SELECT GETDATE(); SELECT @@IDENTITY 由于这些数据只需保留最近一天的数据,因此建立一个SQL作业来定期删除数据,删除脚