JAVA网络编程

UDP

UDP 有点类似于寄快递。把要传送出去的东西打包成一个包裹,写上地址,就寄出去了。但是在运送过程中也和寄快递一样存在安全隐患,也许是地址写错了,也许是发生了其他意外,导致接收方接收不到信息。小白在学习过程中就遇到了这样的问题。

接收端接收不到发送端发送的东西

纠结很久,才发现又是因为一个低级的错误。

我打开 cmd 先进行发送端的发送,后结束发送程序。再打开接收端接收。
1
解决方法:打开两个cmd,一边进行发送,一边进行接收
2

TCP

TCP 较于 UDP 更为安全可靠,它通过三次握手来确定对方的存在:
客户端:在?
服务端:我在
客户端:好的。我发条消息给你。
这样就保证了对方能准确地接收到消息。
在学习过程中,我把 TCP 的传送过程和旅游活动进行了类比,方便记忆。

客户端——游人

  • 客户端绑定服务端的ip地址和端口——游人出游确定景点以及景区的入口
  • 创建Socket输出流和输入流——游人要与景区通信,进行买票或者咨询之类的操作。景区也可以告诉游人一些相关信息
  • 关流——行程结束,不必再与景区保持联系

服务端——景区

  • 确定端口——景区要有一个固定的入口
  • 获取客户端ip——景区获取游人的联系方式
  • 创建输出流和输入流——与游人进行通信
  • 关流——游人结束行程,不再联系

尝试制作一个聊天程序

  • 分析需要用到的知识点(会将这些知识重新梳理在下一篇博文~):
  1. 界面设计
  2. 多线程
  3. 网络编程
  4. IO流
  5. 集合

基于现有知识所需要解决的问题

  1. 如何获取对方的ip地址
    一开始,我陷入了思维的误区,也是因为知识面不够广,认为每个用户在使用该程序时都以自己为服务端,他人为客户端。
    那么通信过程就是服务端和客户端的通信。那么,在未建立起联系的时候,客户端如何进行获取服务端ip地址进而对其绑定呢?
    久思不得其解。最后在陈大神的点拨下才恍然大悟,只要固定一个服务端,用户只使用客户端即可啊!
  2. 该程序在使用时是否需要同时打开客户端和服务端
    不需要。用户只需要打开客户端。并且要先启动服务端之后再打开客户端。
  3. 如何识别发送者的身份
    在聊天过程中,要如何分辨发送消息的人是谁呢?ip 地址时常改变,无法进行用户名的绑定。让用户每次使用都输入自己的名字又过于繁琐。陈大神说,还有另外一种地址叫做 MAC。通常情况下不会发生改变而且具有唯一性。
    不过,我的解决方法是:用户输入用户名之后我获取用户名,每当该用户发送消息都在前面加上该用户名。
    当然,这里可以考虑用Map集合的来保存ip和用户名,保证两者的关系的绑定
  4. 无法正常显示发送者的ip
    服务端进行遍历socketList集合获取客户端的ip地址。
    3;
    如图所示,不管是谁发送的消息,显示的都是我本机的 ip 地址。
    原代码:
    在类里面宏定义:Socket s= null;,进行遍历时:
    1
    2
    3
    4
    5
    for(Socket socket : newServer.socketList){
    PrintStream ps = new PrintStream(socket.getOutputStream());
    String ip = socket.getInetAddress().toString();
    ps.println(ip+"说:"+content);
    }

有兴趣查看完整代码可来我的 github 逛逛喔~
后来居然是学 php 的陈大神帮我 debug……
for循环里面,重新定义的 socket 覆盖了前面的 s,导致每次获取的都是本机地址。
修改后的代码:

1
2
3
4
5
for(Socket socket : newServer.socketList){
PrintStream ps = new PrintStream(socket.getOutputStream());
String ip = s.getInetAddress().toString();
ps.println(ip + "说:" + content);
}

  1. 进行文件传输和保存的时候,如何判断这是一个文件以做出不同于聊天消息的处理方式呢?
    举个例子,这里有一个 txt 文件,里面写着 ”今天天气真好” 发送一个 TXT 文件,那么其他用户就要接收文件,
    4
    而不是在聊天框显示出 ”今天天气真好”;
    5
    所以如何让程序进行识别判断呢?
    在客户端这边设置在发送文件的时候,默认发送一个标识符,比如说:发出一个字符 ’a’;
    在服务端设置如果收到的消息是 ’a’,就认为这是一个文件,于是告诉所有的用户:准备接收文件啦,快使用你们的 SaveFile 方法来保存这个文件。
  2. bufferedreader 的 readline 方法发生阻塞
    接触过 IO流 的小伙伴们一定都很熟悉读取文件的方式
    1
    2
    3
    4
    5
    BufferedReader bufr = new BufferedReader(FileReader fr = new FileReader("buf.txt"););
    String line = null;
    while((line = bufr.readLine())!= null){
    System.out.println(line);
    }

在客户端,我也使用了这种方法进行读取,并规定,当 line = null 的时候,就说明文件读取完毕,可以退出 while 循环。
可是在使用的过程中就出现了一个问题:

1
2
3
4
5
while(true){
//此处省略代码
System.out.println("未退出循环");
if(line ==nullbreak;
}

这个判断程序一直判断为假。明明已经读取到了文件末尾,为什么不等于空呢?而且,按常理来讲,如果判
断为假,那么应该一直进行 while 死循环,一直输出 ”未退出循环”。然而并没有,它输出完文件的内容之后就不再进行其他操作了。
查阅了很多资料,才知道,原来 readline 方法读取到文件末尾并不会返回 null
这是一个阻塞的方法,读到末尾它会一直等待输入。当流被关闭时,才会返回 null
可是这里怎么可能去关闭流呢?关了就收不到信息了呀。
大神突然来了一句:你给它加个判断呀!就像判断它是不是文件一样,你也可以判断它是不是读到了文件末尾,是的话就直接退出循环嘛。
!!!折腾了我两天的 bug 居然就这样子解决了……
当然啦,如果以字符的形式而不是以行的形式读取就没有问题啦~另,JAVA 1.7 也推出了无阻塞的 IO流。可以自行查看开发手册啦~
需要注意的是,这里读取文件的方法应该使用封装后的流,BufferedWriter bufw = new BufferedWriter (new FileWriter("buf.txt"));,如果使用以下代码,会发现文件保存的时候总是少了最后一行。没有关闭fr流,最后一行存储在缓冲区,没有真正写入到文件里面去

1
2
3
4
5
6
fr = new FileReader("IO");
FileWriter fw = new FileWriter("copy_1.txt");
int ch =0;
while((ch = fr.read())!=-1){
fw.write(ch);
}

时间过得太久……遇到的好多 bug 都忘了……暂时先这样吧……整个小程序还有很多不足的地方,后续会进行修改完善并将代码更新上传到 github。
github地址
客户端代码文件是:newClient4
服务端代码文件是:newServer3
唔,要是哪里写错了,欢迎来吐槽喔~(反正我也没有开启评论功能,你想吐了吐不了~哈哈)

您的支持将鼓励我继续创作!