在咱们的TCP API中,也是主要是涉及到两个类:
1)ServerSocket:主要是给TCP服务器来进行使用的;
2)Socket:我们既需要给客户端来进行使用,也需要给服务器来进行使用;
这样就是说我们是不需要使用专门的类来进行表示传输的包,因为我们主要面向的是字节流,我们是以字节为单位进行传输的
咱们在进行创建实例的时候,我们希望把ServerSocket的实例叫做ListenSocket,listen的原意就叫做监听,因为在我们的JAVA中的Socket是体现不出来接听的含义的,但是实际上它还有监听的效果;
如果有连接accept方法就会返回一个Socket对象,也就是说进一步的客户端和服务器的交互就交给Socket了;
1)也就是说咱们的ServerSocket就是一个接线员,他并不会进行负责具体的办理业务,而是把这个业务交给其他人来进行处理;
2)每当我们的ServerSocket通过accept方法感知到了有一个客户端来和我们的服务器建立连接了,那么就会创建出一个Socket对象来进行通信,服务器针对每一个客户端,都会创建出一个Socket来进行通信;
ServerSocket和Socket之间的关系:
一:比如说我以后想去买房,刚出门迎面就走来了一个西装革履的小哥哥,这个小哥哥就带着我来到了楼盘售楼部,这里面是人山人海,这个小哥哥进去之后就打了声招呼,喊出了一个年轻的小姐姐;
二:这个小哥哥就说:这个小姐姐是咱们这个楼盘里面的置业顾问,由它来给你介绍这个该楼盘的销售情况,然后这个小哥哥就走了,他会继续回到这个马路上面,回到马路牙子上,继续去拉其他的人,因为他还是要负责和其他的客户端建立连接;
比如说如果这个小哥哥又拉到了一个人,还是会分配小姐姐的
三:然后我就和这个小姐姐不断地进行交流,由此可见小哥哥就是ServerSocket(只有一个人),小姐姐(多个)就是Socket,买房的人就是一个客户端(数据),对于咱们的每一个想要买房的人,都需要自动分配一个Socket(小姐姐),同时我也是一个Socket实例;
这里面就分成了两步,一个是专门负责数据连接,一个是专门负责数据通信
而我就相当于是客户端的Socket对象,我首先要和服务器和ServerSocket进行连接,最后才可以和服务器专门进行分配的Socket对象通过字节流或者是字符流来进行数据传输;
1)由于咱们的TCP是有连接的,我们一进入到循环是不可以能开始读取数据,而是需要先进行和客户端建立连接,再进行传输数据,先进行接电话,而接电话的前提是有人给你打电话;只有说在我们的客户端Socket中通过构造方法指定服务器的IP地址和端口号,这就相当于有人给你打电话,而咱们的服务器里面的ServerSocket中的accept方法就会感知到,并进行三次握手进行连接,就是相当于是接电话;
2)咱们的accept操作,如果说此时没有客户端和你建立连接,这个accept方法就会阻塞,直到有人向我们当前的服务器建立了连接;
3)因为我们的客户端的请求的主机可能有多份,所以我们服务器针对每一个客户端主机都会有一个Socket对象来进行处理;4)UDP的服务器进入主循环之后,就尝试用receive读取请求了,这是无连接的
5)但是我们的TCP是有连接的,首先需要做的事,先建立起连接,相当于客户端的代码是货品,就要通过一条公路来把这些货发送过去
6)当服务器运行的时候,是否有客户端来建立连接,不确定,如果客户端没有建立连接,accept就会进行阻塞等待,也就是说咱们的TCP必须先通过accept方法先建立好连接才可以进行传输数据,而我们的UDP完全不管37,21就直接进行发送数据
1)在一开始服务器进行启动的时候,我们就需要指定一个端,,后续客户端要根据这个端口来进行访问。服务器的IP地址默认是主机IP
2)后续客户端进行访问服务器的时候,目的IP就是服务器的IP。不需要我们服务器的开发者来进行绑定了,只要我们确定服务器在一台电脑,IP地址就是确定了;
在服务器里面获取到客户端的IP地址和端口号
1)我们可以直接通过ServerSocket实例调用accept方法返回的Socket对象的:
getInetAddress()方法获取到IP地址;
2)我们可以直接通过ServerSocket实例调用accept方法返回的Socket对象的getPort()方法就可以来进行获取到端口号;
1)第一个类:ServerSocket,创建的是实例是listensocket,需要给listenSocket关联上一个端口号,他的构造方法是ServerSocket(int port),还是向服务器指定一个端口;
2)他的其中的accept这样的方法和TCP有连接有着很大的关系,accept就相当于接电话这个操作,监听指定端口,相当于在外场拉客(买房子在场外拉客,来和我们的业务人员沟通一下买房呗),把一个内核建立好的链接交给Socket代码来处理;
listenSocket是ServerSocket创建的实例,它主要的作用是:
1)调用accept方法,是用来与另一台主机进行连接,确定是有连接的,如果主机一直无法建立连接,就会出现阻塞
2)把主机传输过来的数据交给我们的climentSocket进行处理,场外拉客的人直接把客户交给业务人员
3)也就是说客户端尝试建立连接,首先是服务器的操作系统这一层和客户端进行一些相关的流程,先把这个连接建立好;
3)第二个类 Socket,在服务器收到客户端建立连接后,返回的服务器端Socket,它主要是在内场给客户端Socket提供具体的服务,把一台主机传输过来的数据进行解析,并进行返回;
那么我们具体的两个Socket是如何进行通信的呢?
咱们这里面针对的TCPSocket的读写就和文件读写是一模一样的;
1)咱们在进行读Socket文件的数据,就是相当于是在读取网络上面别的主机给咱们发送过来的数据
2)咱们在向Socket文件中写数据的时候,就是相当于是在网络上面向目标主机发送数据
服务器:
1)再使用Socket对象的getInputStream和getoutputStream对象得到一个字节流对象,这时就可以进行读取和写入了;此时的读操作可以调用inputstream.read()里面传入一个字节数组,然后再转化成String,但是比较麻烦,判定结束也是比较麻烦的,直到读到的结果是-1我们才会结束循环,所以我们优先使用Scanner来进行读取,这还是要多些循环,我们使用inputStream.read()的时候,就相当于是读取客户端发送过来的数据
2)我们可以直接通过Socket对象的.getInputStream()来进行获取到流对象,但是具体读的时候Scanner 来进行具体的读,new Scanner(InputStreaam),就巧妙地读到了客户端的请求,在调用scan.next(),写的时候,直接利用Printstream new Printstream()构造方法里面直接写Outputstream,当调用println方法的时候,就默认写回到客户端了
当我们使用outputStream.write()方法的时候,就相当于向我们的客户端返回了数据
客户端过程:全程只需要使用Socket对象
1)创建一个socket对象,创建的时候同时指定服务器的IP和端口号,然后把它们传入到socket中,这个过程就会让客户端和服务器建立连接,这就是三次握手,这个过程就是内核完成的;
2)这时客户端就可以通过Socket对象中的getInputStream和getOutputstream,来得到一个字节流对象,这时就可以与服务器进行通信了;
3)在读的时候,要注意此时只写一个Socket文件,直接把服务器的端口号和IP地址,传入进去进行构造,就自动地和客户端进行了连接;此时还要有InputStreamSocket和OutstreamSocket;
针对于关闭文件来说:
1)对于accept的返回值对应Socket这个文件,是有一个close方法的,如果打开一个文件后,是要记得去关闭文件的,如果不关闭,就可能会造成文件资源泄漏的情况,一个socket文件写完之后本应是要关闭的,但是咱们前面写的UDP程序是不可以关闭的,当时的socket是有生命周期的,都是要跟随整个程序的,当客户端不在工作时,进程都没了,PCB也没了,文件描述符就更没有了
2)咱们的TCP服务器有一个listenSocket,但是会有多个clientsocket,可以说每一个客户端,每一个请求都对应一个climentSocket,如果这个客户端断开链接了,对应的clientSocket也就需要销毁了
1)在我们写TCP服务器的时候,我们都针对了这里面的climentSocket(Socket创建的实例)关闭了一下,但是我们对于listenSocket(ServerSocket创建的实例)却没有进行关闭,直到服务器进行关闭;
2)同时在UDP的代码里面我们也没有针对DatagramSocket对象和DatagramPacket来进行关闭
catch (IOException e) { e.printStackTrace(); }finally { clientSocket.close(); //listenSocket.close();在这里面是不能进行关闭的 }
1)我们关闭的目的是为了释放资源,释放资源的一个前提是这个资源已经不再进行使用了,对于咱们的UDP的程序和ServerSocket来说,这些Socket都是贯穿程序始终的,只要程序启动运行我们就要用到,什么时候咱们的服务器进程关闭,什么时候不用;但是咱们的服务器针对每一个客户端的Socket文件什么时候客户端断开链接了,啥时候就不会再进行使用了
2)这些实例什么时候不用?啥时候咱们的服务器进行关闭,啥时候不用;
3)咱们的这些资源最迟最迟也就会随着进程的退出一起进行释放了,进程才是操作系统分配资源的最小单位,那么这个进程曾经进行申请的资源也就没有了;
4)但是咱们的climentSocket的生命周期是很短的,针对咱们的每一个客户端程序,都要进行创建一个climentSocket,当我们的对应的客户端断开连接之后,咱们的服务器的对应的客户端的climentSocket对象也就永远不会再进行使用了,我们就需要关闭文件释放资源,咱们的climentSocket对象有很多,每一个客户端都对应一个Socket对象,我们就需要保证,每一次进行处理完成的连接就必须进行释放;
下面的这些代码我们本质上读的是Socket文件:
我们通过调用Socket对象里面的getInputStream()方法,我们就可以进行获取到对应的流对象
全双工双向通信,我们既可以读,也是可以写的;
下面是客户端的代码:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
class Request{
int serverport=0;
String serverIp=null;
Socket socket=null;
public Request(String serverIp,int serverport)throws IOException {
this.serverIp=serverIp;
this.serverport=serverport;
this.socket=new Socket(serverIp,serverport);
}
public void start()throws IOException {
//1从键盘也就是控制台上读取请求
Scanner scan = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
System.out.println("请输入你的请求内容");
System.out.println("->");
String request = scan.next();
if (request.equals("goodbye")) {
System.out.println("即将退出客户端");
break;
}
//2把这个从键盘读取的内容,构造请求发给服务器
PrintStream printStream=new PrintStream(outputStream);
printStream.println(request);
//在这里我们怀疑println不是把数据发送到服务器中了,而是放到缓冲区里面了,我们刷新一下缓冲区,强制进行发送
printStream.flush();//如果不刷新,服务器无法及时收到数据
//3我们从服务器那边读取响应,并进行解析
Scanner scanreturn=new Scanner(inputStream);
String response=scanreturn.next();
//4我们把结果显示在控制台上面
String string=String.format("请求是 %s,回应是 %s",request,response);
System.out.println(string);
}
}catch(IOException e)
{
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
//此处构建request对象的时候就相当于是和服务器建立连接
Request request=new Request("127.0.0.1",9090);
request.start();
}
}
1)让Socket创建的同时,就与服务器建立了链接,相当于拨电话的操作,这个操作对标于服务器中的climentSocket.accept接电话操作,我们客户端的IP地址就是本机的IP地址,咱们的端口号是由系统自动进行分配的;
2)我们在这里面传入的IP和端口号的含义表示的是,不是自己进行绑定,而是表示和这个IP端口建立连接
下面是服务端的代码:
我们一般是先启动服务器,再启动客户端;
1)当启动服务器的时候,此时的服务器就会阻塞到accept这里,此时还没和客户端建立连接;
2)客户端在这里再启动,就会调用Socket的构造方法,在构造方法中就会和服务器建立连接;
3)当客户端和服务器建立连接后,accept方法就会返回;
4)procession方法中,就会进入循环,尝试读取数据,就会阻塞在if语句里面的next方法中,当客户端真正发送请求为止;
5)此时的客户端也进入到start中的next方法中,也会阻塞在next方法中,等待用户在键盘上输入一个字符串(标准键盘输入)
6)当下的状态是客户端阻塞在用户往建盘中输入数据,服务器阻塞在等待客户端的请求if语句里面的hasNext;
7)接下来,用户输入数据的时候,此时客户端的阻塞就结束了,然后会发送一个数据给服务器(Outputstream),同时服务器就会从等待读取客户端请求的状态中恢复过来(结束if语句里面的阻塞),执行后面的procession逻辑;
7)当客户端发送完请求之后,又会在第二个传递InputStream里面的scanner的next方法里面进行阻塞,等待服务器的响应的数据;
8)服务器响应并把数据发送回去后,客户端才会在传递scanner的next的等待中返回,最后再来进行打印相关信息;
咱们的服务器要想拿到客户端的端口号就要通过climentSocket(Socket创建的实例)
IP地址:climentSocket.getInetAddress().toString();
端口号:climentSocket.getPort();
9)我们要使用printWriter中的println方法而不是write();
public class TCPServer {
ServerSocket listenSocket=null;
public TCPServer(int serverPort) throws IOException {
listenSocket=new ServerSocket(serverPort);
}
public void start() throws IOException {
System.out.println("TCP服务器开始进行启动");
while(true)
{
Socket clientSocket= listenSocket.accept();
procession(clientSocket);
}
}
private void procession(Socket clientSocket) throws IOException {
System.out.printf("我们这一次客户端请求的IP地址是%s 端口号是%d",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream= clientSocket.getOutputStream()){
//我们来进行循环处理请求,来进行处理响应,我们的一台主机是要给服务器发送多次请求的
Scanner scanner=new Scanner(inputStream);
while(true)
{
if(!scanner.hasNext())
{
//客户端断开连接的代码
System.out.printf("客户端断开连接%s %d",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
}
我们在这里面使用Scanner是更方便的,如果说我们不使用Scanner就需要进行使用原生的inputStream中的read方法就可以了,只不过我们需要创建一个字节数组,然后使用stringbulider来进行拼接
// 1.读取请求并进行解析,读取Socket网卡
String request= scanner.next();
//2.根据请求计算并执行逻辑,我们创建process方法执行
String response=process(request);
//3.帮我们写的逻辑返回给客户端,为了方便起见,我们直接使用PrintWriter来进行对OutputStream来进行包裹一下
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
//4打印信息
System.out.printf("[客户端的端口号是%d 客户端的IP地址是%s],请求数据是%s,响应数据是%s",clientSocket.getPort(),clientSocket.getInetAddress(),request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
clientSocket.close();
listenSocket.close();
}
}
private String process(String request) {
return request+"我爱你";
}
public static void main(String[] args) throws IOException {
TCPServer server=new TCPServer(9099);
server.start();
}
}
1)针对写操作,我们要进行刷新缓冲区,如果没有这个刷新,那么我们的客户端时不能第一时间获取到这个响应结果
2)对于每一次请求,都对应着一个Socket,我们都要创建一个procession方法来进行处理,我们接下来就来处理请求和响应
3)这里面的针对TCP Socket的文件读写是和文件读写是一模一样的,我们在里面主要是对socket文件来进行读和写,TCP和UDP是全双工,我们既可以读Socket文件,也可以写Socket文件
1)对于咱们的UDP的DatagramSocket来说,咱们构造方法指定的端口,就是表示自己想要绑定的端口;
2)对于咱们的TCP的ServerSocket来说,构造方法指定的端口,也是表示自己要进行绑定哪一个端口;
3)对于咱们的TCP的Socket来说,对于构造方法指定的端口,表示要进行连接的服务器的IP地址和端口号,创建实例,就相当于是进行了三次握手,就和服务器建立了连接;