超基础的网络编程02:基于TCP的Socket通信

网络编程02:基于TCP的Socket通信

标签: 网络编程


Socket通信模型

我们已经知道,网络编程实际上就是Socket编程。因此我们的网络通信就是要在客户机和服务器的两端各自建立一个Socket,然后进行通信。Socket通信模型如下图所示:

Socket通信模型

  • 服务器端

    1. 创建服务器端类

    2. 创建一个ServerSocket的对象,通过构造器指明自身的端口号

    3. 调用accpet()方法,此时线程会阻塞,等待并接收连接请求

    4. 接收到请求后,accpet()方法返回一个Socket对象;

    5. 调用ServerSocket对象的getInputStream()或getOutputStream()来进行通信

    6. 关闭相应的资源

  • 客户机

    1. 创建客户机类

    2. 创建一个Socket的对象,通过构造器指明服务端的IP地址,以及其接收程序的端口号

    3. 调用ocket对象的getOutputStream()方法来发送数据,方法返回OutputStream的对象

    4. 具体写入过程

    5. 关闭相应资源

Socket类详解

public class Socket extends Object implements Closeable
该类实现客户端套接字(也称为“套接字”)。 套接字是两台机器之间通讯的端点。

构造方法

  • Socket()
    创建一个未连接的套接字,并使用系统默认类型的SocketImpl。

  • Socket(InetAddress address, int port)
    创建流套接字并将其连接到指定IP地址的指定端口号。

  • Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
    创建套接字并将其连接到指定的远程端口上指定的远程地址。

  • Socket(String host, int port)
    创建流套接字并将其连接到指定主机上的指定端口号。

  • Socket(String host, int port, InetAddress localAddr, int localPort)
    创建套接字并将其连接到指定远程端口上的指定远程主机。

说明:

我们最常用的就是Socket(String host, int port) ,指定要连接的主机名和端口号。

除了第一个不带参的构造方法,其余的构造方法都试图与服务器创建连接,如果连接成功,就返回Socket对象;如果因为某些原因连接失败,则会抛出异常

除了第一个不带参数的构造方法, 其他构造方法都需要在参数中设定服务器的地址, 包括服务器的IP地址或主机名, 以及端口

获取Socket的信息的方法

在一个Socket 对象中同时包含了远程服务器的IP地址和端口信息, 以及客户本地的IP 地址和端口信息。
此外, 从Socket对象中还可以获得输出流和输入流,分别用于向服务器发送数据, 以及接收从服务器端发来的数据。

以下方法用于获取Socket的有关信息:

  • InetAddress getInetAddress(): 获得远程服务器的IP地址.

  • int getPort(): 获得远程服务器的端口.

  • InetAddress getLocalAddress(): 获得客户本地的IP地址.

  • int getLocalPort(): 获得客户本地的端口.

  • InputStream getInputStream(): 获得输入流. 如果Socket 还没有连接, 或者已经关闭, 或者已经通过 shutdownInput() 方法关闭输入流, 那么此方法会抛出IOException.

  • OutputStream getOutputStream(): 获得输出流, 如果Socket 还没有连接, 或者已经关闭, 或者已经通过shutdownOutput() 方法关闭输出流, 那么此方法会抛出IOException.

对Socket进行操作的方法

  • void bind(SocketAddress bindpoint)
    将套接字绑定到本地地址。

  • void close()
    关闭此套接字。

  • void connect(SocketAddress endpoint)
    将此套接字连接到服务器。

  • void connect(SocketAddress endpoint, int timeout)
    将此套接字连接到具有指定超时值的服务器。

  • void shutdownInput()
    将此套接字的输入流放置在“流的末尾”。 发送到套接字的输入流侧的任何数据都被确认,然后静默丢弃。
    如果您在套接字上调用此方法后从套接字输入流读取,则流的available方法将返回0,其read方法将返回-1 (流结束)。

  • void shutdownOutput()
    禁用此套接字的输出流。 对于TCP套接字,任何先前写入的数据将被发送,随后是TCP的正常连接终止序列。 如果在套接字上调用shutdownOutput()之后写入套接字输出流,则流将抛出IOException。

ServerSocket类

Socket类代表一个客户端套接字,即任何时候你想连接到一个远程服务器应用的时候你构造的套接字,现在,假如你想实施一个服务器应用,例如一个HTTP服务器或者FTP服务器,你需要一种不同的做法。这是因为你的服务器必须随时待命,因为它不知道一个客户端应用什么时候会尝试去连接它。为了让你的应用能随时待命,你需要使用java.net.ServerSocket类。这是服务器套接字的实现。

ServerSocket和Socket不同,服务器套接字的角色是等待来自客户端的连接请求。一旦服务器套接字获得一个连接请求,它创建一个Socket实例来与客户端进行通信。

服务器端建立套接字的时候,要建立ServerSocket类的对象,调用其accpet()方法,等待并接收连接请求。此时线程会阻塞,当接收到连接请求的时候,accept()方法会返回一个Socket对象。

public class ServerSocket extends Object implements Closeable
这个类实现了服务器套接字。 服务器套接字等待通过网络进入的请求。 它根据该请求执行一些操作,然后可能将结果返回给请求者。

构造方法

  • ServerSocket()
    创建未绑定的服务器套接字。

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

  • ServerSocket(int port, int backlog)
    创建服务器套接字并将其绑定到指定的本地端口号,并指定了积压。

  • ServerSocket(int port, int backlog, InetAddress bindAddr)
    创建一个具有指定端口的服务器,侦听backlog和本地IP地址绑定。

主要方法

  • Socket accept()
    侦听要连接到此套接字并接受它。

实际上,accept方法是唯一用到的方法。

简单例题

客户端给服务端发送信息,服务端输出此信息到控制台上。

服务器端:

package charNet;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TestTCPServer {
    public static void server() {
        ServerSocket serverSocket = null;
        Socket s = null;
        InputStream is = null;
        try {
            //1. 创建一个ServerSocket的对象,通过构造器指明自身的端口号
            serverSocket = new ServerSocket(9090);
            //2.调用其accpet()方法,返回一个Socket对象
            s = serverSocket.accept();
            //3.调用ServerSocket对象的getInputStream()获取一个从客户端发送过来的输入流
            is = s.getInputStream();
            //4.对获取的输入流进行的操作
            byte[] b = new byte[20];
            int len;
            while ((len = is.read(b)) != -1) {
                String string = new String(b,0,len);
                System.out.println(string);
            }
            System.out.println("收到来自于" + s.getInetAddress().getHostAddress() + "的连接");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //5.关闭相应的资源
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                s.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        server();
    }
}

客户端:

package charNet;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;

//客户端给服务端发送信息,服务端输出此信息到控制台上
//网络编程实际上就是Socket编程
public class TestTCPClient {

    //客户机
    public static void client() {
        Socket socket = null;
        OutputStream os = null;
        try {
            //1. 创建一个Socket的对象,通过构造器指明服务端的IP地址,以及其接收程序的端口号
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 9090);

            //2.getOutputStream():发送数据,方法返回OutputStream的对象
            os = socket.getOutputStream();
            //具体的写入过程
            os.write("我是客户端".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭相应资源
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        client();
    }
}

我们要注意一点,我们在进行运行时,我们要先运行服务器端,在运行客户机端。

运行结果:

//服务器端的控制台输出:
我是客户端
收到来自于127.0.0.1的连接

进阶例题

客户端给服务端发送信息,服务端收到信息后给客户端一个响应,服务器端和客户端都输出信息到控制台上。

服务器端;

package charNet;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class LoginServer {
    public static void main(String[] args) {
        try {
            //1. 创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
            ServerSocket serverSocket = new ServerSocket(8889);
            //2. 调用ServerSocket的accept()方法开始监听,等待客户端的连接
            System.out.println("***服务器即将启动,等待客户端的连接***");
            Socket socket = serverSocket.accept();
            //3. 获取输入流,用来读取客户端发送的信息
            InputStream is = socket.getInputStream(); //字节输入流
            InputStreamReader isr = new InputStreamReader(is);  //将字节流转换为字符流
            BufferedReader br = new BufferedReader(isr);    //为输入流添加缓冲
            String info = null;
            while ((info = br.readLine()) != null) {    //循环读取客户端的信息
                System.out.println("我是服务器,客户端说:" + info);
            }
            socket.shutdownInput(); //关闭输入流

            //4. 获取输出流,相应客户端的请求
            OutputStream os = socket.getOutputStream();
            PrintWriter pw = new PrintWriter(os);   //包装为打印流
            pw.write("服务器表示欢迎!");
            pw.flush();

            //5. 关闭资源
            pw.close();
            br.close();
            isr.close();
            socket.close();
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.println("服务器结束");
        }
    }
}

客户机端:

package charNet;

import java.io.*;
import java.net.Socket;

public class LoginClient {
    public static void main(String[] args) {
        try {
            //1. 客户端创建一个Socket,指定服务器地址和端口
            Socket socket = new Socket("localhost",8889);
            //2. 获取输出流,向服务器端发送信息
            OutputStream os = socket.getOutputStream(); //字节输出流
            PrintWriter pw = new PrintWriter(os);   //将输出流包装为打印流
            pw.write("用户名:user1;密码:123");
            pw.flush();
            socket.shutdownOutput();    //关闭输出流

            //3. 获取输入流,用来读取服务端的响应信息
            InputStream is = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String info = null;
            while ((info = br.readLine()) != null) {    //循环读取客户端的信息
                System.out.println("我是客户端,服务端说:" + info);
            }

            //4. 关闭资源
            br.close();
            pw.close();
            os.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }

    }
}

服务器端输出:

***服务器即将启动,等待客户端的连接***
我是服务器,客户端说:用户名:admin;密码:123
服务器结束

客户机端输出:

我是客户端,服务端说:服务器表示欢迎!

综合例题

应用多线程来实现服务器与多客户端之间的通信:两个客户端分别发送数据,服务器为每个客户端单独建立一个线程来通信,接收各客户端发送的数据后,写到一个共享文档中。

多线程服务器的基本步骤:

  1. 服务器创建ServerSocket,循环调用accept()等待客户端连接
  2. 客户端创建一个socket并请求和服务器端连接
  3. 服务器端接受客户端请求,创建socket与该客户建立专线连接
  4. 建立连接的两个Socket在一个单独的线程上对话
  5. 服务器端继续等待新的连接
客户机A
package charNet;


/**
 * Created by japson on 9/14/2017.
 */

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

/**
 * 1. 创建一个Socket对象,用来指定服务器的地址和端口号
 * 2. 创建一个输出流,用于发送数据
 * 3. 创建一个输入流,用于从文件读取数据到程序,在将数据写到输出流中
 * 4. 获取服务器的响应
 */
public class TestFileClientA {
    public static void fileClientA() {
        Socket socket = null;
        BufferedWriter bw = null;
        BufferedReader br = null;
        InputStream is = null;
        try {
            socket = new Socket(InetAddress.getByName("localhost"),9999);
            OutputStream os = socket.getOutputStream();
            bw = new BufferedWriter(new OutputStreamWriter(os));

            br = new BufferedReader(new FileReader("ClientA.txt"));
            String str = null;
            while ((str = br.readLine()) != null) {
                bw.write(str);
                bw.newLine();
                bw.flush();
            }
            socket.shutdownOutput();

            is = socket.getInputStream();
            byte[] b = new byte[1024];
            int len;
            while ((len = is.read(b)) != -1) {
                String str1 = new String(b,0,len);
                System.out.println(str1);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                bw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        fileClientA();
    }
}
客户机B
package charNet;


/**
 * Created by japson on 9/14/2017.
 */

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

/**
 * 1. 创建一个Socket对象,用来指定服务器的地址和端口号
 * 2. 创建一个输出流,用于发送数据
 * 3. 创建一个输入流,用于从文件读取数据到程序,在将数据写到输出流中
 * 4. 获取服务器的响应
 */
public class TestFileClientB {
    public static void fileClientB() {
        Socket socket = null;
        BufferedWriter bw = null;
        BufferedReader br = null;
        InputStream is = null;
        try {
            socket = new Socket(InetAddress.getByName("localhost"),9999);
            OutputStream os = socket.getOutputStream();
            bw = new BufferedWriter(new OutputStreamWriter(os));

            br = new BufferedReader(new FileReader("ClientB.txt"));
            String str = null;
            while ((str = br.readLine()) != null) {

                //newLine()不能放在flush()之后??
                bw.write(str);
                bw.newLine();
                bw.flush();
            }
            socket.shutdownOutput();

            is = socket.getInputStream();
            byte[] b = new byte[1024];
            int len;
            while ((len = is.read(b)) != -1) {
                String str1 = new String(b,0,len);
                System.out.println(str1);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                bw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        fileClientB();
    }
}
服务器
package charNet;

/**
 * Created by japson on 9/14/2017.
 */

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 要求:创建一个多线程服务器,循环监听客户端的请求,在收到客户端发送的文件后保存到本地,并给出响应
 * 对于多线程,什么是共享数据?socket?
 * 步骤:
 * 1. 创建一个ServerSocket对象,用来确定端口号
 * 2. 循环调用其accept方法监听,并返回监听到的Socket对象
 * 3. 启动子线程类
 * 线程类,采用runnable接口:
 * 1. 在类中定义一个Socket,但是不初始化,需要用构造函数来给它初始化
 * 2. 在run()方法中创建一个输入流,将接收到的信息输入到程序中
 * 3. 在创建一个输出流,将程序中的数据写到本地
 * 4. 返回给服务器一条信息(通过输出流)
 */
class ServerThread implements Runnable {
    Socket socket;
    FileWriter fileWriter;

    public ServerThread (Socket socket,FileWriter fw) {
        this.socket = socket;
        this.fileWriter = fw;
    }
    public void run() {
        BufferedReader br = null;
        BufferedWriter bw = null;
        OutputStream os = null;
        try {
            InputStream is = socket.getInputStream();
            InputStreamReader isr = new InputStreamReader(is);
            br = new BufferedReader(isr);

            bw = new BufferedWriter(fileWriter);
            String str = null;
            while ((str = br.readLine()) != null) {
                bw.write(str);
                bw.flush();
                bw.newLine();
            }

            os = socket.getOutputStream();
            os.write("服务器已经成功接受你发送的文件!".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {

//            try {
//                os.close();
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
//            try {
//                bw.close();
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
//            try {
//                socket.close();
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
        }


    }
}
public class TestFileServer {
    public static void fileServer() {
        try {
            ServerSocket serverSocket = new ServerSocket(9999);
            Socket socket = null;
            int count = 0;
            FileWriter fw = new FileWriter("共享文档.txt",true);
            System.out.println("---服务器启动,等待客户端的连接---");
            while (true) {
                count++;
                socket = serverSocket.accept();
                ServerThread serverThread = new ServerThread(socket,fw);
                Thread thread = new Thread(serverThread);
                thread.setName("线程"+ count);
                thread.start();
                System.out.println("客户端的数量:" + count);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        fileServer();
    }
}
问题
  1. 客户端,将本地的内容写入到输出流时,使用缓冲流。若newLine()放在flush()之后,则异常;放在flush()方法之前,则可以。问题是:在其他代码中,newLine()可以放在flush()之后,为什么这里不可以?

  2. 服务器端的SeverThread类的run()方法,如果在该方法中关闭IO流和socket,那么第一个客户端启动后,第二个客户端启动时,服务器报错。如果不关闭资源,则可以。
    问题是:如何关闭资源

更新 问题2:

问题2的提出有误。
首先,对于每一个线程,关闭相关资源是可行的也是必要的,每个线程中的IO流都是独立的,相互不影响的。
其次,根据对不同代码的注释,最后将问题代码定位在所在:

bw.close();
os.close();

因为我们是不同的线程中的数据,对同一共享文档进行操作。因此要想关闭bw对象bw = new BufferedWriter(fileWriter);,就同时关闭了fileWriter对象。但是fileWriter对象是在主线程中定义的,每一个线程传递的数据都要写入文档,因此不能在线程内进行关闭。

最后,注释掉bw.close();后,运行成功

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在信号处理领域,DOA(Direction of Arrival)估计是一项关键技术,主要用于确定多个信号源到达接收阵列的方向。本文将详细探讨三种ESPRIT(Estimation of Signal Parameters via Rotational Invariance Techniques)算法在DOA估计中的实现,以及它们在MATLAB环境中的具体应用。 ESPRIT算法是由Paul Kailath等人于1986年提出的,其核心思想是利用阵列数据的旋转不变性来估计信号源的角度。这种算法相比传统的 MUSIC(Multiple Signal Classification)算法具有较低的计算复杂度,且无需进行特征值分解,因此在实际应用中颇具优势。 1. 普通ESPRIT算法 普通ESPRIT算法分为两个主要步骤:构造等效旋转不变系统和估计角度。通过空间平移(如延时)构建两个子阵列,使得它们之间的关系具有旋转不变性。然后,通过对子阵列数据进行最小二乘拟合,可以得到信号源的角频率估计,进一步转换为DOA估计。 2. 常规ESPRIT算法实现 在描述中提到的`common_esprit_method1.m`和`common_esprit_method2.m`是两种不同的普通ESPRIT算法实现。它们可能在实现细节上略有差异,比如选择子阵列的方式、参数估计的策略等。MATLAB代码通常会包含预处理步骤(如数据归一化)、子阵列构造、旋转不变性矩阵的建立、最小二乘估计等部分。通过运行这两个文件,可以比较它们在估计精度和计算效率上的异同。 3. TLS_ESPRIT算法 TLS(Total Least Squares)ESPRIT是对普通ESPRIT的优化,它考虑了数据噪声的影响,提高了估计的稳健性。在TLS_ESPRIT算法中,不假设数据噪声是高斯白噪声,而是采用总最小二乘准则来拟合数据。这使得算法在噪声环境下表现更优。`TLS_esprit.m`文件应该包含了TLS_ESPRIT算法的完整实现,包括TLS估计的步骤和旋转不变性矩阵的改进处理。 在实际应用中,选择合适的ESPRIT变体取决于系统条件,例如噪声水平、信号质量以及计算资源。通过MATLAB实现,研究者和工程师可以方便地比较不同算法的效果,并根据需要进行调整和优化。同时,这些代码也为教学和学习DOA估计提供了一个直观的平台,有助于深入理解ESPRIT算法的工作原理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值