hi,大家好,今天为大家带来TCP协议的相关知识
这里写目录标题
- 认识TCP的相关方法
- 实现TCP版本的回显服务器
- 实现多线程版本的TCP回显服务器
- 实现线程池版本的TCP回显服务器
- 认识TCP方法
认识TCP的相关方法
实现TCP版本的回显服务器
实现多线程版本的TCP回显服务器
实现线程池版本的TCP回显服务器
认识TCP方法
TCP也有两个核心的类
Socket和SeverSocket
SeverSocket是给服务器用的
Socket的话,客户端可以用,服务器也可以用
先来看看ServerSocket
ServerSocket() 创建未绑定的服务器套接字。
ServerSocket(int port) 创建绑定到指定端口的服务器套接字。
但我们一般在写服务器的时候都会指定服务器端口号
就像饭店,也得有一个固定位置
Socket accept() 侦听要连接到此套接字并接受它。
accept就是接收的意思,客户端向服务器发起连接请求,在内核中进行连接,accept这里是应用程序层面的接受,就是把连接好的连接拿出来让应用程序连起来,这里先简单的认为是连接,后面讲到TCP的三次握手四次招手再具体介绍
ServerSocket是创建TCP服务端的API
下面再来看看Socket
Socket() 创建一个未连接的套接字,并使用系统默认类型的SocketImplort。
Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号。
public Socket(String host,
int port)
throws UnknownHostException,
IOException
创建流套接字并将其连接到指定主机上的指定端口号。
用于从该套接字读取字节的输入流。
用于将字节写入此套接字的输出流。
InputStream和OutputStream是字节流,TCP是面对字节流的
InputStream是读数据,也就是从网卡接收
OutputStream是写数据,也就是从网卡发送
方法就认识这么多,下面来写TCP版本的回显程序
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: WHY
* Date: 2023-04-07
* Time: 20:43
*/
public class TCPEchoSever {
//severSocket就是外场拉客的小哥
//clientSocket就是内场服务的小姐姐
//severSocket只有一个,clientSocket会给每个客户端都分配一个
private ServerSocket serverSocket=null;
public TCPEchoSever(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while(true){
Socket clientSocket=serverSocket.accept();
processConnection(clientSocket);
}
}
//通过这个方法处理一个连接
//读取请求
//根据请求计算响应
//把响应返回给客户端
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());//得到IP和端口号
//try()这种写法,括号中允许有多个流对象,用;来分割
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()) {
Scanner scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while(true){
//1.读取请求
if(!scanner.hasNext()){
//读取的流到了结尾(对端关闭)
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),
clientSocket.getPort());
break;
}
//直接使用scanner读取一段字符串
String request=scanner.next();
//2.根据请求响应
String response=process(request);
//3.把响应写回到客户端,不要忘了响应也要戴上换行
printWriter.println(response);
System.out.println(response);
System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress(),
clientSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoSever tcpEchoSever=new TCPEchoSever(9090);
tcpEchoSever.start();
}
}
我们来运行一下程序
先启动服务器
看到客户端也启动了,但是输入却没有响应,这是有问题的
为什么客户端输入消息,但是客户端服务器没有任何响应呢
原因就是我们进行TCP的通信媒介是网卡,网卡读取速度比硬盘还要慢,因此为了提高IO效率,我们一般先把数据放到缓冲区上,然后再手动刷新,再到网卡上
这里的打印,是从网卡上读取数据打印的,但是打印是无效的,因为此时数据在缓冲区,需要刷新一下,也就是使用printWriter的flush方法
同样在服务器的
打印响应也要进行刷新
上代码
服务器
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: WHY
* Date: 2023-04-07
* Time: 20:43
*/
public class TCPEchoSever {
//severSocket就是外场拉客的小哥
//clientSocket就是内场服务的小姐姐
//severSocket只有一个,clientSocket会给每个客户端都分配一个
private ServerSocket serverSocket=null;
public TCPEchoSever(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while(true){
Socket clientSocket=serverSocket.accept();
processConnection(clientSocket);
}
}
//通过这个方法处理一个连接
//读取请求
//根据请求计算响应
//把响应返回给客户端
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());//得到IP和端口号
//try()这种写法,括号中允许有多个流对象,用;来分割
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()) {
Scanner scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while(true){
//1.读取请求
if(!scanner.hasNext()){
//读取的流到了结尾(对端关闭)
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),
clientSocket.getPort());
break;
}
//直接使用scanner读取一段字符串
String request=scanner.next();
//2.根据请求响应
String response=process(request);
//3.把响应写回到客户端,不要忘了响应也要戴上换行
printWriter.println(response);
printWriter.flush();
System.out.println(response);
System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress(),
clientSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoSever tcpEchoSever=new TCPEchoSever(9090);
tcpEchoSever.start();
}
}
客户端
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: WHY
* Date: 2023-04-07
* Time: 21:16
*/
public class TCPEchoClient {
private Socket socket=null;
public TCPEchoClient(String serverIp,int port) throws IOException {
//这个操作相当于让客户端和服务器建立tcp连接
//这里的连接连上了,服务器的accept就会返回
socket=new Socket(serverIp,port);
}
public void start(){
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
PrintWriter printWriter=new PrintWriter(outputStream);
Scanner scannerFromSocket=new Scanner(inputStream);
while(true){
//1.从键盘上读取用户输入的内容
System.out.print("->");
String request=scanner.next();
//2.把读取的内容构造成请求,发送给服务器
//注意,这里的发送,是带有换行的
printWriter.println(request);
printWriter.flush();
//3.从服务器读取响应
String response=scannerFromSocket.next();
//4.把响应结果显示到控制台上
System.out.printf("req: %S; resp: %s\n",request,response );
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient=new TCPEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
运行结果
现在就正常了
我们来总结一下TCP通信过程
1.首先要开启服务器,start这里,然后运行到accept这里,阻塞等待
等待客户端的请求
2.客户端启动时,调用Socket方法,和服务器在内核中建立连接,连接成功以后,服务器的accept就返回了
3.服务器这边进入processConnection方法,尝试从客户端读取请求,但是由于此时用户还没有输入,所以读取操作也会阻塞等待
3.客户端这边往下执行时,从控制台读取用户输入,也会阻塞,因为用户可能不会立即输入
4.当用户输入,客户端发请求出去,客户端代码继续执行,到读取服务器响应时,再次阻塞
5.服务器收到客户端的请求,从next这里返回,执行process方法,执行println,把响应写回给客户端
6.客户端这边收到服务器的响应打印到控制台上,同时进入下一次循环,等待用户输入
6.服务器这边回到循环开始的地方,继续尝试获取客户端的请求,然后阻塞等待
对于缓冲区和网卡之间的切换再来说一说
在发数据的时候,必须通过网卡发送,应用程序层面无法直接通过网卡发送数据
接收数据的时候,应用软件无法直接通过网卡接收
那么怎么办呢,在tcp通信中,是存在缓冲区这样的概念的,我们通过缓冲区进行发送和接收
下面来画个图
我们先来画跨主机通信的,那么就需要两个网卡
传输的数据是需要套接socket的,可以把socket当做一个文件,套接tcp数据报进行传输,就像水是在水管里运输的,水管就是相当于socket,水相当于要传输的数据
send:
客户端发出的数据,要先通过socket进行包装,通过socket对象的getOutputStream方法获取与这个socket相关的outputStream对象,使用write()方法写入到缓冲区,然后将发送缓冲区的数据送到网卡,网卡将数据发到服务器端的接收缓冲区,再发给服务器
receive:
服务器在收到数据的时候,处理完数据,就又通过socket,发送到服务器这边的发送缓冲区,发送缓冲区发给网卡,然后交给客户端这边,客户端通过socket对象的getInputStream方法获取与这个socket相关的inputStream对象,客户端就可以通过read()方法从接收缓冲区读取数据
下图是在同一个主机通信的,就使用一个环回网卡就行!!!
发送数据和接收数据过程是一样,就不赘述了
现在这个程序是没有问题了,但是在互联网的世界中,我们知道肯定是有很多客户端要访问服务器
那么这个代码这样写是不支持多个客户端同时访问的,我们来看看
在idea勾选,就可以在idea同时运行很多个客户端
可以看到,我们在客户端1输入请求,是有响应的,在其他的客户端上输入请求,是没有响应的
这里其实是一个bug.是代码的bug
我们来分析一下
当客户端1来的时候,操作系统从内核拿出连接,然后接受连接,调用processConnectino方法,进入processConnecion方法那么只有当客户端1下线其他客户端才能相继连接,我们想要循环处理客户端1的数据,还要循环accept,接受其他的客户端请求,那么我们要咋样解决这个问题呢?
用多线程
这样写就可以实现一边循环处理原来客户端的请求,还能循环接收其他的客户端进行accept
注意:这里的线程是一个一个创建的,就是串行创建,而在执行的时候是并发的
咋样通俗理解呢
拿售楼来说,一个小哥拉来客户1号,交给一个小姐姐1,进行服务,在小姐姐1服务客户1的时候,这个小哥又拉来一个客户2,再来一个小姐姐2,服务,依次类推,这些客户之间,小姐姐之间是互不影响的,是一起执行的,就是这样理解的了
我们把完整代码给大家
客户端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: WHY
* Date: 2023-04-07
* Time: 20:43
*/
public class TCPEchoSever {
//severSocket就是外场拉客的小哥
//clientSocket就是内场服务的小姐姐
//severSocket只有一个,clientSocket会给每个客户端都分配一个
private ServerSocket serverSocket=null;
public TCPEchoSever(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while(true){
Socket clientSocket=serverSocket.accept();
Thread thread=new Thread(()->{
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
//通过这个方法处理一个连接
//读取请求
//根据请求计算响应
//把响应返回给客户端
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());//得到IP和端口号
//try()这种写法,括号中允许有多个流对象,用;来分割
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()) {
Scanner scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while(true){
//1.读取请求
if(!scanner.hasNext()){
//读取的流到了结尾(对端关闭)
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),
clientSocket.getPort());
break;
}
//直接使用scanner读取一段字符串
String request=scanner.next();
//2.根据请求响应
String response=process(request);
//3.把响应写回到客户端,不要忘了响应也要戴上换行
printWriter.println(response);
printWriter.flush();
System.out.println(response);
System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress(),
clientSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoSever tcpEchoSever=new TCPEchoSever(9090);
tcpEchoSever.start();
}
}
服务器
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: WHY
* Date: 2023-04-07
* Time: 21:16
*/
public class TCPEchoClient {
private Socket socket=null;
public TCPEchoClient(String serverIp,int port) throws IOException {
//这个操作相当于让客户端和服务器建立tcp连接
//这里的连接连上了,服务器的accept就会返回
socket=new Socket(serverIp,port);
}
public void start(){
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
PrintWriter printWriter=new PrintWriter(outputStream);
Scanner scannerFromSocket=new Scanner(inputStream);
while(true){
//1.从键盘上读取用户输入的内容
System.out.print("->");
String request=scanner.next();
//2.把读取的内容构造成请求,发送给服务器
//注意,这里的发送,是带有换行的
printWriter.println(request);
printWriter.flush();
//3.从服务器读取响应
String response=scannerFromSocket.next();
//4.把响应结果显示到控制台上
System.out.printf("req: %S; resp: %s\n",request,response );
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient=new TCPEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
线程频繁的创建和销毁是耗费资源的,我们升级代码,采用线程池,避免线程销毁,用完放到线程池里面,更加高效
看看完整代码
客户端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created with IntelliJ IDEA.
* Description:
* User: WHY
* Date: 2023-04-07
* Time: 20:43
*/
public class TCPEchoSever {
//severSocket就是外场拉客的小哥
//clientSocket就是内场服务的小姐姐
//severSocket只有一个,clientSocket会给每个客户端都分配一个
private ServerSocket serverSocket=null;
public TCPEchoSever(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
ExecutorService executorService = Executors.newCachedThreadPool();
System.out.println("服务器启动!");
while(true){
Socket clientSocket=serverSocket.accept();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
//通过这个方法处理一个连接
//读取请求
//根据请求计算响应
//把响应返回给客户端
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());//得到IP和端口号
//try()这种写法,括号中允许有多个流对象,用;来分割
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()) {
Scanner scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while(true){
//1.读取请求
if(!scanner.hasNext()){
//读取的流到了结尾(对端关闭)
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),
clientSocket.getPort());
break;
}
//直接使用scanner读取一段字符串
String request=scanner.next();
//2.根据请求响应
String response=process(request);
//3.把响应写回到客户端,不要忘了响应也要戴上换行
printWriter.println(response);
printWriter.flush();
System.out.println(response);
System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress(),
clientSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoSever tcpEchoSever=new TCPEchoSever(9090);
tcpEchoSever.start();
}
}
服务器
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: WHY
* Date: 2023-04-07
* Time: 21:16
*/
public class TCPEchoClient {
private Socket socket=null;
public TCPEchoClient(String serverIp,int port) throws IOException {
//这个操作相当于让客户端和服务器建立tcp连接
//这里的连接连上了,服务器的accept就会返回
socket=new Socket(serverIp,port);
}
public void start(){
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
PrintWriter printWriter=new PrintWriter(outputStream);
Scanner scannerFromSocket=new Scanner(inputStream);
while(true){
//1.从键盘上读取用户输入的内容
System.out.print("->");
String request=scanner.next();
//2.把读取的内容构造成请求,发送给服务器
//注意,这里的发送,是带有换行的
printWriter.println(request);
printWriter.flush();
//3.从服务器读取响应
String response=scannerFromSocket.next();
//4.把响应结果显示到控制台上
System.out.printf("req: %S; resp: %s\n",request,response );
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient=new TCPEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
补充:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以 多次收发数据
这就是这期的所有内容了,我们下期再见,886!🌸🌸🌸