ServerSocket API(只给服务器使用的类)
给服务器端使用的类 ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
ServerSocket(int port)
创建一个服务端流套接字Socket,并绑定到指定端口
ServerSocket 方法:
Socketaccept()
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并且阻塞等待
void close()
关闭此套接字
Socket API(既会给服务器用,也会给客户端用)
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
Socket(String host,int port)
创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接Socket 方法:
尝试和指定服务器建立连接
Socket 方法:
InetAddress getinetAddress()
返回套接字所连接的地址
InputStream getinputStream()
返回此套接字的输入流
OutputStream getOutputStream()
返回此套接字的输出流
服务器客户端代码示范:
服务器:
UDP
package net;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/**
* Created with IntelliJ IDEA.
* Description:
* User: admin
* Date: 2023-01-15
* Time: 14:54
*/
public class UdpEchoServer {
private DatagramSocket socket = null;
//参数的端口数表示咱们的服务器要绑定的端口,目的就是为了让客户端明确访问主机上的哪个程序
public UdpEchoServer(int port) throws SocketException{
socket = new DatagramSocket(port);
//这里给了参数,这是因为服务器需要有稳定的端口号
//服务器
}
//通过这个方法启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while(true){
//循环里面处理一次请求,使用一个死循环去随时处理请求
//1.读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//new byte[4096]是给packet申请内存空间
socket.receive(requestPacket);
//receive的参数是一个输出型参数,调用receive的时候,就需要构造一个空的datagrampacket对象,然后把对象交给receive在receive里面
//负责把网卡读到的数据给填充到这个对象中
//请求还没有发来的时候receive就会阻塞
//把这个datagrampacket对象转成字符串,方便去打印
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应
String response = process(request);
//通过这个方法计算响应
//3.把响应写回到客户端
DatagramPacket responPacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
//通过requestPacket.getSocketAddress()就可以从客户段拿到当前发来包裹的ip和端口号
//与上面的构造没啥区别,上面的是空的内存空间用于构造,下面的是使用带有数据的空间进行构造
socket.send(requestPacket);
//打印一个日志,记录当前情况
System.out.printf("[%s:%d] req: %s; resp: %s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
//依次获取ip地址,端口号,请求,响应
}
}
//当前写的是一个回显服务器,响应数据就和请求是一样的
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
客户端:
package net;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: admin
* Date: 2023-01-15
* Time: 15:28
*/
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int ServerPort;
//两个参数一会在发送数据的时候用例
//暂时先把这俩参数存起来,以备后用
public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
//客户端的构造方法,参数是传一个服务器的ip和服务器的端口
socket = new DatagramSocket();
//这里没有指定参数,因为客户端不需要有稳定的端口号
//客户端给服务器发送一个数据,客户端自己的主机,源ip
//客户端没有手动绑定一个端口,操作系统会自动分配一个空闲的端口
//这里不指定端口不是说没有端口,而是让系统自动指定一个空闲的端口
//手动指定端口可能不确定这个端口是否空闲
//服务器在自己手里,而客户端在用户手里,程序员可控端口,而用户电脑就有可能占用端口,使用客户端不会指定端口
//端口是一个16为的整数,0-65534,我们一般使用1024-65535,小于1024的为知名端口号,已经被一些著名的应用程序占用了
//假设serverip是形如1 2 3 4 这种点分十进制的表示方式
this.serverIP = serverIP;
this.ServerPort = serverPort;
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true){
//1.从控制台读取用户输入的内容
System.out.print("->");
String request = scanner.next();
//2.构造一个UDP请求,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.serverIP),this.ServerPort);
//这里构造了一个有数据的packet,用ip和端口描述发送给谁
socket.send(requestPacket);
//3.从服务器读取UDP响应数据并解析
DatagramPacket responPacket = new DatagramPacket(new byte[4096], 4096);
//构建空的packet
socket.receive(responPacket);
//只有服务器的响应回来了这里的receive才会解除阻塞,唤醒并且拿到数据
String response = new String(requestPacket.getData(),0, responPacket.getLength());
//把packet东西取出来构建到字符串中
//4.把服务器的响应显示到控制台上
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
如何通过服务器运行多个客户端:
在Configuration里面点击开
然后去找到右上角的绿色的Modify options,点开
选择Allow multiple instance
这样下来,就可以多开客户端了,如图
客户端和服务器需要对应一个端口号才可以相互产生反应,不同的服务器不能占用同一个端口
TCP
package net_test;
import com.sun.corba.se.spi.activation.Server;
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: admin
* Date: 2023-01-17
* Time: 21:24
*/
public class TcpEchoClient {
//客户端使用这个socket对象来创建连接
private Socket socket= null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
//服务器建立连接需要知道服务器位置
//和上次写的udp连接差别很大
socket = new Socket(serverIP,serverPort);
}
public void start(){
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true){
//1.从控制台读取数据,构造出一个请求
System.out.println("->");
String request = scanner.next();
//2.发送请求给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.printf(request);
printWriter.flush();
//3.从服务器读取响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//4.把响应显示到界面上
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1,9090",9090);
client.start();
}
}
package net_test;
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: admin
* Date: 2023-01-17
* Time: 21:24
*/
public class TcpEchoServer {
//代码中会涉及到多个socket对象,使用不同的名字来区分
private ServerSocket listenSocket = null;
public TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
//1.先调用accept来接受客户端的连接
Socket clientsocket = listenSocket.accept();
//2.再去处理这个链接
processConnection(clientsocket);
}
}
private void processConnection(Socket clientsocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线",clientsocket.getInetAddress().toString(),clientsocket.getPort());
//处理客户端请求
try(InputStream inputStream = clientsocket.getInputStream();
OutputStream outputStream = clientsocket.getOutputStream()){
//在inputstream里面读数据就是在网卡中读数据
//在outputstream里面写数据就是在网卡中写数据
while (true){
//读取请求并解析
Scanner scanner = new Scanner(System.in);
if(!scanner.hasNext()){
//读完了,连接可以断开了
System.out.printf("[%s %d] 客户端下线!",clientsocket.getInetAddress().toString(),
clientsocket.getPort());
break;
}
String request = scanner.next();
//根据请求计算响应
String response = process(request);
//把响应写回给客户端
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,确保数据通过网卡发送出去了
printWriter.flush();
System.out.printf("[%s:%d] resp: %s\n",clientsocket.getInetAddress().toString()
,clientsocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
//为啥这个地方要关闭,因为socket本身也是一个文件,一个进程同时打开的文件数目也是有上限的
//因为这里的clientsocket是在while循环里面被反复创建,每次创建都要消耗一个文件描述符
//因此就需要把不再使用的clientsocket及时释放掉
clientsocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
传输层中
UDP
无连接
不可靠传输
面向数据保
全双工
TCP
有链接
可靠传输
面向字节流
全双工
TCP如何体现可靠传输:
1.首先确认应答,把这个应答报文叫做ACK,并且引入了序号和确认序号
注意:ACK(应答)和响应是不相同的,前者是告诉发送方收到消息,后者是携带业务上的数据
2.超时重传,针对确认应答做出的补充,因为传输的过程中可能会丢包,例如数据包丢了,ack丢了。发送方并不会区分这两个情况,所以发送方会触发超时重传操作。
注意:在TCP讲的时候,我们总会说发送方和接收方,而不是客户端和服务器,因为客户端既可以是发送方也可以是接受方,同理服务器。
3.连接管理
连接如何建立,如何断开——通过三次握手,四次挥手
滑动窗口——TCP保证可靠性前提下尽可能提高效率的操作
这里的提高效率也只是亡羊补牢罢了,因为本身就是牺牲了效率换取可靠性,因此TCP的效率不可能超过UDP。
朴素的应答就是一问一答,你发一句我来应答然后你发下一句,还需要等别人的回应。
提升效率的方法就是不等回应了直接发下一条,但是又不是完全不等,每次都是批量的发一波信息,然后在等一波ACK,然后再发下一波。
我们把不需要等待,就可以直接发送的数据的两,称为窗口大小。
批量发送4条数据,批量等待4条ack,此时的窗口大小就是4000(以字节为单位)
4个数据中收到最前端的一个ACK,就继续发一条数据,而不是等所有ACK都到了才统一发下一组。
例如:在发送1001-5000的数据的时候,中间的2001这个ACK在到达主机A的时候,此时主机就继续发送5001-6000,等待范围变成2001-6000(2001这个数字表示2001之前的数据已经收到)
因此效率的高低取决于窗口的大小,窗口越大,效率就越高,窗口越小,效率就越低
因此如果窗口无限大,发送方完全不需要等待ACK,那么效率比就和UDP一样了
丢包
1.传的数据丢了
如果1001-2000这个数据丢失的时候。开始的时候发送方是不知道的,仍然在继续发。主机的ACK还是在向A索要1001,在连续索要后,A意识到丢包了1001,所以重传了1001,主机B收到后,因为之前把后面的数据都传过去了(假设是7001),所以确认序号就是7001
前面丢掉的是1001-2000,2001-7000这些数据都没有丢,但是B始终差了1001-2000,A重传后之前的数据就被补齐了,因此继续从7001开始即可。
上述也叫做快速重传(搭配滑动窗口机制的超时重传),数据很多,批量传输,此时自然是遵守快速重传的方式,数据很少,仍然是按照超时重传的方式进行
2.响应的ACK丢了
不要紧,即使丢了50%,对于可靠传输也没有任何影响 ,因为后续的ACK可以代表前面的ACK已经收到了,所以无伤大雅。但是如果是最后一个ACK丢了,那么就按照超时重传而不是快速重传的方式来处理。
流量控制
TCP滑动窗口越大,发送的速度就会越快,但是接收端很容易吃不消。发送的速度太快了就会导致接收方丢弃一部分数据。
TCP是要保证可靠性的,TCP还得重传这些数据。
流量控制就是在滑动窗口的基础上对发送速率做出限制的机制,也就是限制窗口的大小不要太大。
也就是接收方对于发送方的限制,接收方根据自己的接受能力来反向影响发送方接下来的发送速率
那么接收方的接受速率要怎么量化你。接收方使用接受缓存区的剩余空间大小来作为发送方发送速率(窗口大小)的参考值。
拥塞控制
虽然存在流量控制,但是如果在刚开始阶段就发送了大量的数据,还是会出现问题,因为网络状态可能会比较拥堵,因此TCP引入了慢启动机制,先发送少量数据探探路,摸清网络状况在决定按照多大速度传输数据。
如果不丢包,就要放大拥塞窗口,开始的时候先指数增长(短时期就摸清网络传输底线),达到阈值(初始阈值一般是系统配置)就线性增长。
如果速率达到上限,网络阻塞出现丢包。
窗口大小就会一下子回到最初的值,重复刚才的指数增长,线性增长的过程,同时动态调整一下阈值,阈值调为刚才丢包时候的窗口大小的一半。