TCP (socket api)
TCP提供两个类:
TCP是面相字节流;并不需要像UDP专门一个类构建数据报;是以字节的方式传输。(TCP : 有连接 可靠传输 面向字节流 全双工)
ServerSocket
ServerSocket 是一个在服务器端等待监听来自客户端的连接请求的类。当客户端请求建立连接时,ServerSocket 对象将接受连接请求并创建一个新的 Socket 对象,该对象将与远程主机建立连接。服务器端可以使用连接的 Socket 对象向客户端发送和接收数据。
ServerSocket:给服务器使用的Socket对象
构造方法:
普通方法:
这个方法相当于接电话;建立连接之后会返回一个Socket对象;我们通过这个对象与客户端进行沟通。
Socket
Socket 是用于建立客户端和服务器之间的连接。客户端使用 Socket 对象来与服务器通信。它提供了一组方法,可以使用TCP或UDP协议远程主机进行通信。
Socket:客户端,服务器都有使用的对象
构造方法
通过刚才简单的介绍这两个类和一些方法;进行一个回显服务器TCP版本体验网络通信的这个流程。
服务端实现
import java.io.IOException;//TCP的学习
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class test1 {
public ServerSocket socket1=null;
public test1(int port) throws IOException {//构建这样一个对象;传入一个端口号;因为是本机测试;所以我们在客户端就直接填环回IP
socket1=new ServerSocket(port);//服务器的端口号
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
//接收连接accept方法;如果没有客户端建立连接;这个方法就会阻塞.
//建立连接后会返回一个socket对象;我们通过这个对象与客户端交互
Socket socket2 = socket1.accept();
//两个对象区别;socket1就像中介,通过accept方法把你介绍到socket2,然后由socket2再一顿操作。
//随后就处理这个介绍来的socket2;这个对象会占用文件描述符表,用完需要释放(量多、生命周期短)
processconnection(socket2);
}
}
private void processconnection(Socket socket2) {
//通过介绍过来的对象可以创建输入、输出流对象;放try里能自动释放
try (InputStream s2getinput= socket2.getInputStream();
OutputStream s2output=socket2.getOutputStream()){
Scanner scanner=new Scanner(s2getinput);//读取输入流
while (true){//客户端可能输入123 然后234;多次请求;得要多次响应,就用循环。
//1:读取请求;通过scanner读取
if(!scanner.hasNext()){//当没有下一个数据;说明读完了(客户端关闭连接);使用next是读到空格、空白符结束;但是不返回这些
System.out.println("客户端下线");
break;
}
String request=scanner.next();
//2:根据请求构造响应
String response=process(request);
//3:返回响应结果;正常就得利用OutputStream写回;但是这样有个问题;OutputStream不能直接写字符串
//这里两种方案;把String的字节数组拿出来进行写入;把 s2output转化字符流
//方案1
//String responseData = "Hello, Client!";
//s2output.write(responseData.getBytesUTF-8"));//转成字节数组再进行写入;
//方案2(Reader、Writer是字符流输入输出)
// Writer out = new OutputStreamWriter(s2output, "UTF-8");
//out.write(responseData);
//================================================================之所以能直接写;网卡也是相当于文件IO的操作
//方案2 PrintWriter 是一种字符流输出器,它继承自 Writer 类;子类功能更强大
PrintWriter p=new PrintWriter(s2output);
p.println(response);//使用println写入;带换行能方便接收;flush刷新一下,保证写入的确实发送出去
p.flush();
System.out.printf("[%s:%d] req: %s; resp: %s \n", socket1.getInetAddress().toString(),socket2.getPort(),request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
finally {
// 4:socket2回收这个对象
// try并不是所有的都能使用当前能用但是其它情况可能并不能使用;更合适的做法, 是把 close 放到 finally 里面, 保证一定能够执行到!!
try {
socket2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {//回显服务器;直接简单的返回响应
return request;
}
public static void main(String[] args) throws IOException {
test1 server = new test1(8848);
server.start();
}
}
总体步骤:
创建对象;建立连接;处理请求:getInputStream()把数据读出来、中间还有响应、getOutputStream()把响应数据写回去、关闭这个连接创建的对象。
我们在服务器创建的ServerSocket对象的作用是调用accept方法创建连接(在这里阻塞等待客户端发来连接请求);然后再换成操作这个新的accept返回的socket对象。
客户端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// Socket 构造方法, 能够识别 点分十进制格式的 IP 地址. 比 DatagramPacket 更方便。
// new 这个对象的同时, 就会进行 TCP 连接操作.
socket = new Socket(serverIp, serverPort);
}
public void start() {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 1. 先从键盘上读取用户输入的内容
System.out.print("> ");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("goodbye");
break;
}
// 2. 把读到的内容构造成请求, 发送给服务器.
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);//相当于又在写网卡;然后把数据发送出去
// 此处加上 flush 保证数据确实发送出去了.
printWriter.flush();
// 3. 读取服务器的响应
Scanner respScanner = new Scanner(inputStream);//Scanner会阻塞,直到 inputStream 中有响应数据可读。
String response = respScanner.next();
// 4. 把响应内容显示到界面上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
对整个过程的执行步骤描述:
1:创建socket对象;先输出服务器启动;然后执行到 Socket socket2 = socket1.accept();阻塞等待连接。
2:客户端启动;创建连接 socket = new Socket(serverIp, serverPort);
3:服务器在获取连接后;准备处理连接;把客户端的内容读取出来。在scanner阻塞;
4:客户端在创建连接后继续往下执行;也是执行到scanner;阻塞等待用户输入。3和4的操作先后并不明确;但是也不影响。结果是两边都阻塞到这里
5:客户端两种可能;
一:输入exit/直接结束进程; 服务端Socket socket2 = socket1.accept()就获取不到客户端的socket;因为客户端这个已经下线。所以就没有这个输入流;那么scanner.hasnext也就返回false;就输出 客户端下线,并且结束本次的循环读取;等待下一次新的连接创建
二:正常输入;客户端进行往下直到阻塞scanner读取服务器的响应中。
6:服务器把输入的内容读取;然后再进行响应
7:服务器把响应的结果 通过PrintWriter写回去
8:客户端阻塞结束;读取服务器的响应;并且处理。
9:继续循环;等待你下一次的输入
(我们客户端服务器读取使用hasnext和循环的好处:能分多次输入和响应请求;如果输入 123 345 456.。。一次输入带有空格;就会读到空格为止是一次请求。回车也是如此。。因为这个读取一次读多少个都行;所以正好能用这个划分每次读取。使用next接收正好能识别这个换行符/空白符,并且内容不包含这个东西)
PrintWriter类的介绍:
我们使用PrintWriter写和OutputStream效果是一样的;往同一个地方写,对应的文件描述符表还是同一个,这里写文件就通过网卡发送。
细节补充:
为什么我们客户端、服务器在写的时候都带上\n呢,有什么好处?
TCP协议:面向字节流的协议;一次读多少个字节都可以;接收方怎么知道你这一次输入要读多少个字节?
所以\n就能做到为我们当前代码的请求和响应分割效果。
回车也是/n
我们客户端输入回车不就刚好给这个next低效了;但是我们发送过去的内容可没有回车;不然他会一直读取;不知道读多少个停止;所以有个/n就能刚好低效;next的结束标志。
多线程版本
上述的代码明显不合理;每次一个客户端的请求都得阻塞半天等它输入。我们的服务器肯定得要响应多个客户端;如果有其它客户端你不能让人家客户干等着。所以使用多线程客户端每发起一个请求就创建一个线程;单独的执行流;谁也不影响谁。(主要理解两个线程冲突在哪里发生占线问题)
在两个客户端下输入123然后回车并没有反应
直到我把test3的客户端结束后;test4客户端才能上线
问题出现在:两个线程;占线后;另一个线程都无法上线;
我们的代码相当于循环套循环;一个循环是为了接收多个客户端;另一个循环是为了多次读取和返回同一个客户端的请求、响应。问题在与
一个客户端连接上后;我们的代码跑到第2个循环卡住了;需要等你当前客户端结束后这个(结束客户端进程/输入exit)才能继续执行第一个循环尝试获取下一个连接。解决方案;在第二个循环使用多线程;我们的第一个循环每接收一个连接就创建一个新的请求处理、响应。让我们的代码光在第一个循环里走;一直循环有多少客户端就创建多少个连接。(简单来说就是第二个循环影响第一个循环的执行;accect无法第二次调用到;影响新客户端的连接)
需做如下改动
短连接与长连接:
短连接:客户端每次给服务器发消息就先建立连接;整体流程结束再关闭连接;下次再发送则重新建立连接
长连接:建立连接后;不着急断开;一系列的发送请求和读取响应。如果客户端确实在短时间不需要这个连接再断开。
线程池版本
如果是很多客户端;那么我们就需要反复创建、销毁线程;需要不少开销。三个颜色代表三个版本
IO多路复用:如果线程池还是不够用;IO多路复用充分的利用等待的时间;去做别的事情。一个线程处理很多个客户端。给这个线程安排个集合;这个集合放一堆连接;这个线程负责监听这个集合;哪里有数据来线程就处理哪个连接(这些连接在严格意义上还是有分先后的)操作系统提供一些原生API(select,poll,epoll),java用NIO这样的类封装这些API。
比如我们团队需要获取唱跳rap这样的技能;你我他咱三就兵分三路每个人学一种;而不是咱三先去唱;然后再一起去跳;最后rap。
横向对比:UDP和TCP编程
UDP:(无连接、不可靠传输、面向数据报、全双公)一个socket可以发也可以收;不用建立连接就能收发数据;面向数据报也是可以体现。只是当前无法体现不可靠传输;待网络原理再详细介绍。
TCP:(有连接;可靠传输;面向字节流;全双公)
TCP在这里也是没有体现出可靠传输的;后面网络原理TCP详细介绍;毕竟这个TCP很大一个意义是为了解决可靠传输问题;在后续网络网络原理之TCP讲详细介绍