(一).TCP字节流套接字编程
1.涉及到的类
在使用TCP字节流套接字编程的时候,具体涉及到两个类,一个类是ServerSocket ,另一个类是Socket
ServerSocket主要是针对于服务器来提供的类,Socket主要是针对客户端来提供的类
2.构造方法
(1).ServerSocket

TCP的ServerSocket构造方法和UDP的DatagramSocket构造方法差不多,对于有参数的构造方法也是需要关联上一个端口号
(2).Socket

在创建Socket对象的时候,直接将目的ip(服务器ip)和目的端口(服务器端口)传到Socket对象中
3.其他方法
(1).ServerSocket

对于TCP来说,相对于UDP,TCP是有连接的,这个**accept()**方法就是联通连接的关键操作

关闭服务器方法
(2).Socket


getInetAddress()和getPort()方法是用于返回源ip和源端口的

当Socket对象使用完成之后,需要进行手动关闭


由于TCP不想UDP一样,有receive()和send()方法,所以TCP是通过InputStream和OutputStream来完成字节流的输入和输出的
4.模拟实现回显服务器和客户端
(1).回显服务器
Ⅰ.创建对象

Ⅱ.写出构造方法

Ⅲ.写出主循环
在主循环中,TCP服务器的通常流程为①.创建连接②.接收请求并解析③.根据请求,计算响应④.把响应返回给客户端

上图是主循环的代码

这里我们之所以可以通过Scanner来获取从clientSocket获得到的inputStream,是因为之前在使用Scanner来进行获取的数据的时候,我们通常在括号中写System.in ,这表示从控制台获取输入,其实本质上就是一个InputStream。所以就说明了,如果在括号中写的是一个文件名,那么就意味着是从文件中获取输入;相对应的如果我写socket.getInputStream(),那么就意味着是从网络中获取输入;如果我写inputStream,那么就是从二进制字节流中获取输入

同样那个PrintWriter也一样
Ⅳ.写出主函数

(2).回显客户端
Ⅰ.创建对象

Ⅱ.写出构造方法

Ⅲ.写出主循环

Ⅳ.写出主函数

(3).细节问题
Ⅰ.Socket对象问题


这两个Socket对象不是同一个Socket对象,这两个Socket对象分别在不同的进程中,甚至在不同的主机上。这两个Socket可以想象成两部电话A,B,从A的话筒说话,B的听筒能听见,从B的话筒说话,A的听筒能听见
Ⅱ.当程序运行起来的时候,发现并没有返回数据

当客户端输入"hhhh"的时候,并没有返回"hhhh",同时服务器也没有进行打印

这是因为println()这个操作,只会将数据读取到"发送缓冲区"中,"发送缓冲区"就相当于一块"内存空间",还没有真正的写入到网卡里。这是因为对于这种IO类的操作,不管是写硬盘还是写网络,都属于比较低效的操作,如果频繁的写那么效率就比较慢,所以为了提高效率,就可以先把要写的数据放到缓冲区中,再统一进行发送,这样就可以减少写网络或硬盘的速度,但是这也会导致一个问题,就是当调用println()这个操作并没有真的发送,而是将数据放到了内存缓冲区中,要想让数据发送出去,就要使用一个"数据刷新"的操作,即通过调用flush()方法进行刷新


在客户端和服务器的println()方法后面都加上flush()方法

通过上面的图片,就可以看到问题已经被解决了
Ⅲ.是否可以将println()改成print() ?
答案是不可以的

当我将println()改成print()的时候,再次运行程序,发现客户端并没有收到服务器返回的响应数据,同时在服务器中也没有打印
这是因为println()在输入结束之后会自动加上"\n",同时在服务器代码进行if判断的时候,判断
!scanner.hasNext()的时候会有一个"\n",所以程序才会继续往下运行
对于hasNext()来说,判断收到的数据中是否包含"空白符","空白符"包括 换行,空格,回车,制表符,翻页符等等,一旦遇到了空白符,才认为是一个"完整的next",在遇到之前,都是会阻塞等待
当我将println()修改成print()之后,其实数据是发过去的,并且服务器也收到了,但是没有真正的去处理
所以这里就是默认一个请求/响应,使用"\n"作为结束标记,对端读的时候也会读到"\n"就结束
由于TCP是以"字节"为单位来进行读数据的,但是实际上一个请求,往往是由多个字节来构成的,具体是多少个字节构成了一个完整的请求,就需要有办法将其标记出来,所以引入分隔符是标记请求边界的一种典型方式,即 将请求和请求之间的标记划分好 涉及到的问题叫做**"粘包问题"**
Ⅳ.clientSocket是否需要关闭?
对于服务器的serverSocket对象是不需要进行关闭的,因为serverSocket是贯穿整个服务器进程的,但是clientSocket就需要进行关闭了,clientSocket对象的进行不是贯穿整个服务器,而是贯穿整个连接,每个客户端都会创建一个新的clientSocket,当每个客户端断开连接,这个对象也就不需要了,那么将其关闭就好了,如果不关闭就会造成文件泄露,当打开到了一定的程度,就会将文件描述符表耗尽,则就无法打开新的了

所以在try的最后搭配一个finally,当连接结束的时候将其关闭即可
Ⅴ.当前服务器是否可以给多个客户端同时提供服务?
首先,先配置一下客户端,允许多个客户端同时打开

通过上面的步骤,就可以打开多个客户端了

当我启动两个客户端的时候,发现,只有其中的一个客户端能和服务器之间完成请求和响应,另一个客户端无法进行

当我将第一个客户端关掉之后,发现,第二个客户端和服务器之间就可以完成请求和响应了
这是因为

解决方法,可以通过多线程或者线程池来进行解决,main主线程只负责accept()方法,每次accept()到一个客户端,就创建一个线程,由新线程负责处理客户端的请求



在使用线程池的时候,一般不会使用fixedThreadPool,因为fixedThreadPool同时处理的客户端连接的数目就固定了,这就有点不科学了,我们并不知道具体有多少个客户端发送请求
注意:无论是多线程还是线程池,都意味着一个客户端对应一个线程,但是一个主机上创建的线程数目是有限的,当到达了具体上限的时候,即使创建再多的线程,CPU的利用率也上不去了,同时还会因为多个线程来导致程序运行效率降低。那么有没有办法来解决一万个客户端或者1000万个客户端的问题?
答案是有的,IO多路复用(IO多路转接),这里不过多介绍,因为JVM中没有原生的IO多路复用api,只不过是把api进行了封装,但是封装之后已经不单单是IO多路复用了