1.两个关键的类
2.ServerSocket API
2.1构造方法
2.2 accept
2.3 close
为什么UDP的客户端服务器中没有这个close方法,因为这个socket的生命周期是与进程一样的,只要服务器运行这,socket就不能释放,当进程结束的时候,持有的资源都会全部释放,包括占有的内存和文件描述符表
3.Socket API
客户端和服务器都要用,TCP传输的是字节流,就是传输字节
3.1 构造方法
根据上述的构造方法,创立一个socket对象就能够和服务器建立连接,相当于拨号操作
3.2 其他方法
这个方法可以获取到socket内部的流对象,通过InputStream,OutputStream进行read和write操作对网卡进行操作
获取到对应的端口号和IP地址
4.代码
服务器
package network;
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;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
//通过accept方法,把内核中已经建立好的连接拿到应用程序中
//建立连接的细节流程都是内核自动完成的,应用程序只需要捡现成的
Socket clientSocket = serverSocket.accept();
//就像买房外面有人去拉客人来,而里面有人专门和客户谈
//serverSocket专门去接受连接,而clientSocket专门去和后续的客户端进行通信
processConnection(clientSocket);
}
}
//通过这个方法,来处理当前的连接
public void processConnection(Socket clientSocket){
//进入方法,先打印一个日志,表示当前有客户端连上了
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(),clientSocket.getPort());
//接下来进行数据的交互
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
//使用try()方法,避免后续用完了流对象忘记关闭
//由于客户端发来的数据可能是多条数据,循环处理
while (true){
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()){
//连接断开了,此时循环就应该结束
System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
//1.读取请求并解析,此处就以next来作为请求的方式.next的规则是,读到空白符就返回
String request = scanner.next();
//空白符:换行,空格,制表符,翻页符,垂直制表符....
//后续客户端发起的请求,会以空白符作为结束标记(此处约定了\n)
//一般会手动约定出,从哪里到哪里是一个完整的数据包
//每次循环一次,就处理一个数据包即可
//上述这里就是约定了使用\n作为数据包的结束标记,就正好可以搭配scanner.next来完成请求的过程
//2.根据请求计算响应
String respons = process(request);
//3.把响应写回到客户端
//可以把String转成字节数组,写入到OutputStream
//也可以使用printwriter 把 OutputStream包裹一下,来写入字符串
PrintWriter printWriter = new PrintWriter(outputStream);
//此处的println不是打印到控制台了,而是写入到outputStream对应的流对象中,也就是写入到clientSocketlim
//自然这个数据也就通过网络发送出去了(发给当前这个连接的另外一端)
//使用println带有\n 也是为了后续 客户端这边 可以使用 scanner.next 来读取数据
printWriter.println(respons);
//此处还要记得有个操作,刷新缓冲区,如果没有这个刷新操作,那么可能当前这个数据还在内存中没写进网卡里面
printWriter.flush();
//4.打印一下这次请求交互过程的内容
System.out.printf("[%s,%d] req=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),request,respons);
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
//在这个地方,进行clientSocket的关闭
//processConnection就是在处理一个连接,这个方法执行完毕,这个连接也就处理完了
clientSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
public String process(String request){
//此处也是写的回显服务器,响应和请求是一样的
return request;
}
}
客户端
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;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
//需要在创建Socket的同时,和服务器建立连接,此时就得告诉Socket服务器在哪
//具体建立连接的细节不需要我们代码手动干预,是内核自动负责的
//当我们new这个对象的时候,操作系统内核就开始三次握手具体细节,完成建立的过程了
socket = new Socket(serverIp, serverPort);
}
public void start(){
//tcp的客户端行为和udp客户端差不多
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
PrintWriter writer = new PrintWriter(outputStream);
Scanner scannerNetwork = new Scanner(inputStream);
while (true){
//1.从控制台读取用户输入的内容
System.out.println("->");
String request = scanner.next();
//2.把字符串作为请求,发送给服务器
//这里使用println,是为了让请求后面带上换行
//也就是和服务器读取请求,scanner.next呼应
writer.println(request);
writer.flush();
//3.读取服务器返回到的响应
String response = scannerNetwork.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();
}
}
4.2 解析
等待客户端发送请求,服务器与客户端建立连接之后,返回一个socket对象,如果没有客户端发送请求就会阻塞
TCP处理连接中接收请求的当遇到空白符的时候才会读取完毕,使用这个scanner直接收到的就是字符串,如果使用inputstream的read()方法还需要将字节转化为字符串,客户端在发送请求的时候,必须在每个请求的末尾加上空白符,这个要求是程序员之间通信细节的约定,TCP是按照字节的方式来进行传输的,实际上我们希望若干个字节能构成一个应用层的数据报,区分一个应用层的数据报就是通过空白符来进行分割
空白符是一类统称,空格,换行回车,制表符,翻页符...
响应也加一个空白符,这样就能区分出一个完整的响应,一个请求对应一个响应,因为这是TCP传输的普遍问题,发请求和返回响应都需要考虑分隔符
阻塞等待请求的到达,1)请求到了有明确的分隔符返回true,2)TCP连接断开了返回false
当单独启动服务器没有启动客户端时,会在accept处产生阻塞,
当启动客户端之后服务器便会解除阻塞接收客户端发来的请求
当结束客户端时,会在服务器程序控制台打印客户端下线,当强制结束客户端进程时,或者调用socket.close()方法时,操作系统内核就会感知到,从而TCP断开连接流程(触发四次挥手)
5.问题
.1 连接
什么意思?
就像去吃海底捞,有时候人太多了需要排号
这个时候内核中的人就可以进应用程序里吃饭了
内核里排队的人就是内核里的连接,应用程序里有位置了,就会从内核中拿一个连接进去
2.clientsocket用完要close
前面写的Datagramsocket,serversocket都没写close,但是没关系
因为datagramsocket和serversocket,都是在程序中只有那么一个对象,生命周期都是贯穿整个程序的
clientsocket则是在循环中,每次有一个新的客户来建立连接,都会创建出新的clientsocket