7.4 (java学习笔记)网络编程之TCP

一、TCP

  1.1 TCP(Transmission Control Protocol 传输控制协议),是一种面向连接的,安全的传输协议,但效率相比于UDP而言比较低。

  TCP传输时需要确保先建立连接之后,再进行传输这样就保证了传输的可靠性。

  java中将TCP封装成了对应的类。

    ServerSocket:服务端

    Socket:客户端

  1.2TCP连接的建立与取消(三次握手与四次挥手)

    连接(三次握手):

      1.初始状态,服务器处于监听状态,主机的传输控制模块(TCB)像服务器发送连接请求,客户端进入同步已发送状态。

      2.服务器受到客服端发送的连接请求,如果同同意连接则向客户端发送确认,服务器进入同步收到状态。

      3.客户端受到确认后,继续给服务器发送确认报文,客户端进入已连接状态。

      后续服务器收到客服端的确认后也进入已建立连接状态。

     建立连接后,客户端和服务器就可以愉快的发送信息了,信息发送完毕后,就要断开连接。

     断开(四次挥手):

      1.客户端发送释放报文,同时停止发送数据主动关闭TCP连接,进入终止等待状态1。

      2.服务器收到释放报文后发送确认,此时服务器进入关闭等待状态。此时客户端到服务器方向的连接就释放了。

      此时TCP进入半连接状态,服务器到客户端的连接未释放,此时服务器还可以将未发送完的数据向客户端发送。

      3.服务器没有数据向客户端发送之后,就会发出连接释放报文等待客户端确认,服务器进入最终确认状态。

      4.客户端收到服务器发送的释放报文后,向服务器发送一个确认报文,服务器进入连接关闭状态。客户端同时进入时间等待(TIME-WAIT)状态。

        此时连接还没有被释放掉。客户端会等待2MSL的时间,然后进入连接关闭状态。至此连接断开完成。

       

      每一条TCP的连接唯一的被两个通信两端的两个端点表示,也就是是四元组(源IP,源端口,目的IP,目的端口),

      而不是单纯的用一个IP地址和端口区别。

      也就意味着一个TCP可以建立多个连接,比如服务器IP是127.0.0.1,端口是8888;

      例如客户端一:127.0.0.1:3389

        客户端二:127.0.0.1:3390

        客户端三:127.0.0.1:3390

      三个连接对应的四元组

      TCP 127.0.0.1:3889 127.0.0.1:8888 ESTABLISHED

      TCP 127.0.0.1:3890 127.0.0.1:8888 ESTABLISHED

      TCP 127.0.0.1:3891 127.0.0.1:8888 ESTABLISHED

      我们可以发现即使目的地IP和端口相同,但本地的端口不同导致整个四元组不同。

      服务器可以建立多个连接,前提是四元组不同。连接中无法出现两个四元组相同的连接。

       TCP可以连接多个客户端,为其每一个客户端创建一个Socket,Socket不同代表不同连接。

      客户端服务器之间通过Socket通信,服务器加上多线程为每一个Socket分配一个线程就可实现并发处理。

      

      参考:1、计算机网络(第四版) 谢希仁编著。

         2、https://www.cnblogs.com/Andya/p/7272462.html

           3、https://blog.csdn.net/sssnmnmjmf/article/details/68486261

二、ServerSocket

    ServerSocket(int port)//创建绑定到指定端口的服务器套字节。

    默认绑定的IP地址是本地的IP地址。

    例如我这里是在个人电脑上面运行,绑定的地址就是当前主机的IP地址。

    当前IP地址可按win键+r输入cmd,然后输入ipconfig -all查看以太网适配器的IPv4地址,后面带有首选的。

    

    也可以认为是绑定到127.0.0.1上,因为当前C/S都是在一台电脑上运行都属于本机访问,

    所以本地测试使用的回环地址(127.0.0.1和本机IP(192.168.190.1)都可以。

    

     2.主要方法

    Socket accept()//监听要对当前对象IP上指定端口的连接,如果发现有连接请求则连接它。

    例如客户端发送一个连接请求到当前服务端的对应端口,则建立客户端与服务端的连接。

    这个监听是一个阻塞式的监听,意思就是说如果没有建立连接的话当前进程就不会继续向下运行。

    成功建立连接后,会返回一个Socket对象,而Socket对象中有获取输入输出流的方法,这时就在

    客户端,服务端之间建立输入输出流管道,两者就可以通过这个管道通信。

三、Socket

     1.构造方法:

    Socket(InetAddress address, int port)

    Socket(String host, int port)
    //创建套字节,并将其绑定到指定的(IP|域名)上的指定端口。

    2.主要方法:

    InputStream getInputStream()//返回当前Socket对象的输入流

    OutputStream getOutputStream()//返回当前Socket对象的输出流

四、例子

  Server:

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

public class Server {
    public static void main(String[] args) throws UnknownHostException, IOException {
        String msg = "欢迎连接到Server!";
        ServerSocket server = new ServerSocket(8880);//绑定到本地IP的8880端口
        Socket socket = server.accept();//阻塞式接收,接收成功建立连接管道
        //连接管道的输出流,即对连接对象(客户端)进行输出。
        BufferedWriter bos = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8"));
        bos.write(msg);//服务器将指定内容发给客户端
        bos.newLine();
        bos.flush();
    }
}

Client:

import java.io.BufferedReader;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.UnknownHostException;

public class Client {
    public static void main(String[] args) throws UnknownHostException, IOException {
        Socket client = new Socket("192.168.190.1",8880);//向指定IP地址的指定端口进行连接
    //    Socket client = new Socket("127.0.0.1",8880);//使用127.0.0.1和使用192.168.190.1都可以完成通信
        //连接成功后,获取连接管道的输入流,即对服务器写入内容进行读取
        BufferedReader isr = new BufferedReader(new InputStreamReader(client.getInputStream(),"UTF-8"));
        String re = isr.readLine();//读取内容
        System.out.println(re);
    }
}
运行结果:
欢迎连接到Server!

先运行Server会进行阻塞式接收,没有建立连接前后面的语句都不会执行。

然后运行Client建立连接后,Server向连接管道中写入数据,Client向连接管道中读取数据。

最后将内容显示到控制台。 

五、简易聊天室

下面结合多线程,和网络编程实现一个简易聊天室。

客户端先将消息发送到服务器,服务接收消息后转发给其他客户端。

每个客户端是一个线程。

基本流程:

1.A客户端读取键盘输入数据,并将其发送到服务器。

2.服务器与A客户端建立连接后,将A客户端放入一个容器,同时将A客户端发送的消息,转发给容器中除A客户端之外的所有客户端。

 服务器中为每一个Socket分配一个线程,就可以实现并发转发所有聊天消息。

3.发送给其他客户端后,其他客户端会读取服务器发送的内容并显示到自己控制台。

Send:读取键盘输入内容并将其发送给服务器

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.Socket;

public class Send implements Runnable {
    private boolean Running = true;
    private DataInputStream dis;//用于读取service返回的消息
    private DataOutputStream dos;//用于向server发送消息
    private BufferedReader br;
    public Send(){
        
    }
    
    public Send(Socket client){
        try {
            dis = new DataInputStream(client.getInputStream());
            dos = new DataOutputStream(client.getOutputStream());
        } catch (IOException e) {
            // TODO Auto-generated catch block
            System.err.println("初始化连接失败!");
            Running = false;
            try {
                dis.close();
                dos.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                System.err.println("关闭异常!");
            }
            
        }
        
    }
    //读取键盘输入信息并返回
    private String reciver(){
        String msg=null;
        br = new BufferedReader(new InputStreamReader(System.in));
        try {
            msg = br.readLine();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            System.out.println("读取用户输入异常!");
            Running = false;
            try {
                br.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
        }
        return msg;
    }
    //将键盘输入信息发送至服务器
    private void send(){
        String msg = reciver();
        try {
            if(msg != null && !msg.equals("")){
                dos.writeUTF(msg);
                dos.flush();
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.err.println("用户发送信息异常!");
            Running = false;
            try {
                dos.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
        }
    }
    
    public void run(){
        while(Running){
            send();
        }
    }
}

reciver:读取服务器发送的数据

package ChatRoom;

import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;

public class Reciver implements Runnable{
    private boolean Running = true;
    private DataInputStream dis;
    
    public Reciver(){
        
    }
    //初始化,获取连接
    public Reciver(Socket client){
        try {
            dis = new DataInputStream(client.getInputStream());
        } catch (IOException e) {
            // TODO Auto-generated catch block
            System.err.println("Client-->Server连接失败!");
            Running = false;
            try {
                dis.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                System.err.println("关闭异常!");
            }
        }
    }
    //读取客户端发送的数据
    private String reciver(){
        String msg=null;
        try {
            msg = dis.readUTF();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            System.err.println("接受客户端消息异常!");
            Running = false;
            try {
                dis.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                System.err.println("关闭异常!");
            }
        }
        return msg;
    }
    
    public void run(){
        while(Running){
            System.out.println(reciver());
        }
    }
}

Client:(192.168.1.1~253) 255.255.255.0

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

public class Client {

    public static void main(String[] args) throws UnknownHostException, IOException {
        // TODO Auto-generated method stub
        Socket client = new Socket("192.168.1.254",8888);//连接服务器
            new Thread(new Send(client)).start();//读取键盘数据并发送给服务器
            new Thread(new Reciver(client)).start();//读取服务器发送回来的消息
    }

}

Server: 192.168.1.254   255.255.255.0

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;

public class Server {
    static List<Server.Connect> allUser;
    public static void main(String[] args) throws IOException{
        allUser = new LinkedList<Server.Connect>();//存储客户端的容器
        ServerSocket serverSocket = new ServerSocket(8888);//设置监听端口
        while(true){//不断接受客户端的连接请求
            Socket con = serverSocket.accept();//获取服务器与客户端的Socket
       //   System.out.println(con.getPort());
            Server server = new Server();//实例化一个服务器
            Server.Connect connect = server.new Connect(con);//创建一个客户端到服务器的连接(socket)
            allUser.add(connect);//将已经连接的客户端放入容器,也可以看做将socket放入服务器
            new Thread(connect).start();//每连接一个客户端(socket)就为其开辟一条线程,一个服务器对应多个客户端。
        }
    }
    
    class Connect implements Runnable{//
        private boolean Running = true;//运行标志位
        DataInputStream dis;
        DataOutputStream dos;
        
        public Connect(){
            
        }
        
        public Connect(Socket client){//客户端连接上服务器后的socket
            try {//初始化获取对客户的读写流
                dis = new DataInputStream(client.getInputStream());
                dos = new DataOutputStream(client.getOutputStream());
            } catch (IOException e) {
                // TODO Auto-generated catch block
                System.err.println("Server-->Client连接失败!");
                try {
                    dos.close();
                    dis.close();
                } catch (IOException e1) {
                    // TODO Auto-generated catch block
                    System.err.println("关闭异常");
                }
            }
        }
        
        public String reciver(){//读取客户端发送的消息
            String msg = null; 
            try {
                msg = dis.readUTF();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                System.err.println("获取客户端信息异常!");
                Running = false;
                try {
                    dis.close();
                } catch (IOException e1) {
                    // TODO Auto-generated catch block
                    System.err.println("关闭异常");
                }
            }
            return msg;//返回读取的消息
        }
        
        public void send(String msg){//将消息发送到输出流dos对应的客户端
            try {
                dos.writeUTF(msg);
                dos.flush();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                System.err.println("发送客户端信息异常!");
                Running = false;
                try {
                    dos.close();
                } catch (IOException e1) {
                    // TODO Auto-generated catch block
                    System.err.println("关闭异常");
                }
            }
            
        }
        
        public void sendOther(){//将消息发送到其它客户端,例如A客户端发送过来的消息,就发送给除A之外的客户端
            String msg = this.reciver();
            System.out.println(msg);
            for(Connect temp : allUser){//遍历存放客户端的容器
                if(temp == this)//如果容器中当前对象时是A,就跳过这次循环,不是则将消息发送到对应的客户端。
                    continue;
                temp.send(msg);//哪一个客户端调用就将消息发给谁,假如这里的temp是B就将调用B中的send,此时发送的输出流是向客户端B写入的。
            }
        }

        @Override
        public void run() {//开启多线程后服务器不断接收客户端消息,然后转发
            // TODO Auto-generated method stub
            while(Running){//运行标志位,如果中途出现读写异常则终止。
                sendOther();
            }
        }    
    }
}

如果是一台电脑上测试,则将客户端中连接服务器的地址修改为127.0.0.1或localhost端口任选(大于1024即可)。

如果是多台电脑测试,例如两台电脑(将两台电脑的网线接口用一根网线连接)。

将其中一台电脑的IP地址修改为192.169.1.245:255.255.255.0,

另外一台IP地址只需和其保持同一网段即可例如(192.168.1.1:255.255.255.0)。(最好禁用其余网卡)

在192.168.1.254上先运行服务器然后运行客户端,在另外一个电脑上运行客户端。

通过控制台输入可以实现聊天。

如果想实现同一网段多个电脑间通信需要用到交换机连接多个电脑。

猜你喜欢

转载自www.cnblogs.com/huang-changfan/p/9948419.html
7.4