网络编程1

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引入了慢启动机制,先发送少量数据探探路,摸清网络状况在决定按照多大速度传输数据。

如果不丢包,就要放大拥塞窗口,开始的时候先指数增长(短时期就摸清网络传输底线),达到阈值(初始阈值一般是系统配置)就线性增长。

如果速率达到上限,网络阻塞出现丢包。

窗口大小就会一下子回到最初的值,重复刚才的指数增长,线性增长的过程,同时动态调整一下阈值,阈值调为刚才丢包时候的窗口大小的一半。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值