1

博客主页

1. TCP

TCP(Transmission Control Protocol)是传输控制协议,一种面向连接的,可靠的,基于字节流的传输层通信协议。

TCP 通信同UDP通信一样,都能够实现两台计算机之间的通信,通信的两端都需要创建Socket对象。

区别在于:

  1. UDP中只有发送端和接收端,不区分客户端与服务端,计算机之间可以任意地发送数据
  2. TCP通信严格区分客户端与服务器端,在通信时,必须先又客户端去连接服务器端才能通信,服务器端不能主动连接客户端,并且,服务器端需要先启动,等待客户端的连接

在JDK中,提供了两个类用于实现TCP通信程序

  1. 客户端:java.net.Socket类表示。创建Socket对象,向服务器端发出连接请求,服务器端响应请求,两者建立连接才能开始通信。
  2. 服务端:java.net.ServerSocket类表示。创建ServerSocket对象,相当于开启了一个服务,等待客户端连接。

2. ServerSocket

JDK中java.net包中提供ServerSocket类,该类的实例对象可以实现一个服务器段的程序。

  1. ServerSocket类提供了多种构造方法:
ServerSocket(int port)
创建绑定到指定端口的服务器套接字。

使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上(参数port就是端口号)

  1. ServerSocket的常用方法:
Socket    accept()
侦听要连接到此套接字并接受它。该方法将阻塞直到建立连接。

InetAddress    getInetAddress()
返回此服务器套接字的本地地址。如果套接字被绑定在closed之前,则该方法将在套接字关闭后继续返回本地地址。

ServerSocket对象负责监听某台计算机的某个端口号,在创建ServerSocket对象后,需要继续调用该对象的accept()方法,接收来自客户端的请求。当执行了accept()方法之后,服务器端程序会发生阻塞,直到客户端发出连接请求,accept()方法才会返回一个Scoket对象用于和客户端实现通信,程序才能继续向下执行.

3. Socket

JDK提供了一个Socket类,用于实现TCP客户端程序。

  1. Socket类同样提供了多种构造方法
Socket(String host, int port)
创建流套接字并将其连接到指定主机上的指定端口号。

使用该构造方法在创建Socket对象时,会根据参数去连接在指定地址和端口上运行的服务器程序,其中参数host接收的是一个字符串类型的IP地址。

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

如果指定的主机是null ,则相当于指定地址为InetAddress.getByName (null) 。 换句话说,它相当于指定回送接口的地址。

该方法在使用上与第二个构造方法类似,参数address用于接收一个InetAddress类型的对象,该对象用于封装一个IP地址。

  1. Socket的常用方法
int    getPort()
返回此套接字连接到的远程端口号。

InetAddress    getLocalAddress()
获取套接字所绑定的本地地址。

void    close()
关闭此套接字。任何线程当前被阻塞在这个套接字上的I / O操作将会抛出一个SocketException 。
关闭此socket也将关闭socket的InputStream和OutputStream 。

InputStream    getInputStream()
返回此套接字的输入流。关闭返回的InputStream将关闭相关的套接字。

OutputStream    getOutputStream()
返回此套接字的输出流。关闭返回的OutputStream将关闭相关的套接字。

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

在Socket类的常用方法中,getInputStream()和getOutStream()方法分别用于获取输入流和输出流。当客户端和服务端建立连接后,数据是以IO流的形式进行交互的,从而实现通信。

4. 实现一个简单的TCP网络程序

了解了Socket 和 ServerSocket这两个类的基本用法,通过下面简单的TCP加深理解。

注意:如果先启动客户端,抛出java.net.ConnectException: Connection refused (Connection refused)异常

4.1 客户端向服务端发送数据

服务端实现:

public class TcpServer {
    public static void main(String[] args) throws IOException {

        System.out.println("Server->启动");
        // 创建ServerSocket对象,并绑定到指定端口为20000
        ServerSocket serverSocket = new ServerSocket(20000);

        // 侦听连接,获取Socket对象
        // accept方法将阻塞直到建立连接。
        Socket socket = serverSocket.accept();

        // 通过socket获取网络输入流
        InputStream is = socket.getInputStream();

        // 从输入流中读取字节数据到buffer中
        byte[] buffer = new byte[1024];
        int len= is.read(buffer);

        String msg = new String(buffer, 0, len);

        // 打印接收到的数据
        System.out.println("Server-> receive msg:" + msg);

        // 关闭资源
        socket.close();

        System.out.println("Server->关闭");
    }
}

客户端实现:

public class TcpClient {
    public static void main(String[] args) throws IOException {
        System.out.println("Client->启动");
        // 创建Socket对象,并连接到指定主机上的指定端口号。
        Socket socket = new Socket("127.0.0.1", 20000);

        // 通过Socket获取网络输出流
        OutputStream os = socket.getOutputStream();

        // 通过输出流写入数据
        os.write("hello tcp!".getBytes());

        //关闭资源
        os.close();

        System.out.println("Client->关闭");
    }
}

4.2 服务端向客户端回写数据

服务端实现:

public class TcpServer {
    public static void main(String[] args) throws IOException {
         //...

        // ====================回写数据====================
        // 通过socket获取网络输出流
        OutputStream os = socket.getOutputStream();
        // 通过网络输出流回写数据
        os.write("hello, 我收到了.".getBytes());

        // ...
    }
}

客户端实现:

public class TcpClient {
    public static void main(String[] args) throws IOException {
        // ...

        // ====================接收服务端回写数据====================
        // 通过Socket获取网络输入流
        InputStream is = socket.getInputStream();

        // 从网络输入流中读取数据
        byte[] buffer = new byte[1024];
        int len = is.read(buffer);

        System.out.println("Client-> receive msg: " + new String(buffer, 0, len));
        
        // ...
    }
}

5. 案例实操-TCP传输初始化配置,基本数据传输实例

5.1 TCP传输初始化配置

TCP客户端Client初始化配置

1. 客户端Socket创建方式

在实际项目实操中,创建客户端Socket时,使用无参数的Socket构造,或者通过Socket(Proxy proxy)构造,这样Socket对象创建成功后,是一个未连接Socket,就可以通过Socket对象进行初始化配置。

private static final int PORT = 20001;
private static final int LOCAL_PORT = 30001;

private static Socket createSocket() throws IOException {
    // 创建一个未连接的Socket对象
    Socket socket = new Socket();
    // 或者使用无代理(忽略任何其他代理配置)的构造函数,等效于空构造函数
    //Socket socket = new Socket(Proxy.NO_PROXY);

    // 将Socket绑定到本地IP地址和端口号
    socket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), LOCAL_PORT));
    return socket;
}

也可以在创建Socket时,指定应该使用什么样的代理转发数据。

// 创建一个通过指定的HTTP代理服务器连接的Socket,数据通过指定的代理转发
Proxy proxy = new Proxy(
        Proxy.Type.HTTP,
        new InetSocketAddress("www.baidu.com", 1080)
);
Socket socket = new Socket(proxy);

下面几种方式创建Socket对象时,在创建时就连接到指定的服务器上,不能做一些初始化配置。

// 创建Socket,并将其连接到指定主机上和指定端口号的服务器上
Socket socket = new Socket("localhost", PORT);

//Socket(InetAddress address, int port)
//创建流套接字并将其连接到指定IP地址的指定端口号。
Socket socket = new Socket(Inet4Address.getLocalHost(), PORT);

//Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
//创建套接字并将其连接到指定的远程端口上指定的远程地址。
Socket socket = new Socket(
        Inet4Address.getLocalHost(), 
        PORT, 
        Inet4Address.getLocalHost(), 
        LOCAL_PORT
);

//Socket(String host, int port, InetAddress localAddr, int localPort)
//创建套接字并将其连接到指定远程端口上的指定远程主机。
Socket socket = new Socket(
        "localhost",
        PORT, 
        Inet4Address.getLocalHost(),
        LOCAL_PORT
);
2. Socket初始化配置

在设置Socket一些初始化配置时,需要注意,在Socket连接后配置将不起作用,必须在连接之前调用。

private static void configSocket(Socket socket) throws SocketException {
    // 设置读取超时时间,单位:毫秒。timeout=0时,无限超时;timeout>0时,与此Socket相关联的InputStream上的read()调用将仅阻止此时间.
    // 如果超时超时,则引发java.net.SocketTimeoutException
    socket.setSoTimeout(2000);

    //Nagle的算法,true启用TCP_NODELAY, false禁用。
    socket.setTcpNoDelay(true);

    // 是否需要在长时无数据响应时发送确认数据(类似心跳包),时间大约为2小时
    socket.setKeepAlive(true);

    // 设置逗留时间(以秒为单位),最大超时值是平台特定的,该设置仅影响关Socket关闭。默认为false,0
    // false, 0: 默认情况,关闭时立即返回,底层系统接管输出流,将缓冲区的数据发送完成
    // true, 0: 立即关闭返回,缓存区数据抛弃,直接发送RST结束命令到对方,并无需经过2MSL等待
    // true, 2: 关闭时最长堵塞2秒,随后按照第二种情况处理
    socket.setSoLinger(true, 2);

    // 是否接收TCP紧急数据,默认为false,禁止接收,在Socket接收的TCP紧急数据被静默地丢弃。
    socket.setOOBInline(true);

    // 设置接收缓冲区区大小
    // 增加接收缓冲区大小可以提高大容量连接的网络I / O的性能,同时可以帮助减少输入数据的积压
    // 需要注意:1.对于客户端Socket,在将Socket连接到服务器之前,必须调用setReceiveBufferSize()
    // 2. 对于ServerSocket接受的Socket,必须通过在ServerSocket绑定到本地地址之前调用ServerSocket.setReceiveBufferSize(int)来完成。
    socket.setReceiveBufferSize(64 * 1024 * 1024);

    // 设置发送缓冲区大小的大小,该值必须大于0
    socket.setSendBufferSize(64 * 1024 * 1024);

    // 注意:在此连Socket连接后调用此方法将不起作用,必须在连接之前调用
    // 设置此Socket的性能参数:
    // connectionTime :一个 int表达短连接时间的相对重要性
    // latency :一个 int表达低延迟的相对重要性
    // bandwidth :一个 int表达高带宽的相对重要性
    // 这三个值只是简单的比较,哪个参数设置的值大偏向谁
    socket.setPerformancePreferences(1, 1, 0);
}

最后在创建Socket并配置后,将此Socket连接到具有指定的服务器。

public static void main(String[] args) throws IOException {
    
    Socket socket = createSocket();
    
    configSocket(socket);
    
    //connect(SocketAddress endpoint, int timeout)
    //将此Socket连接到具有指定超时值的服务器
    socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 3000);
}

TCP客户端ServerSocket初始化配置

1. 客户端ServerSocket创建方式

通常在创建ServerSocket对象时,使用空参数的构造函数,这样后续可以给ServerSocket设置一些配置。

private static ServerSocket createServerSocket() throws IOException {
    // 创建未绑定的服务器套接字
    ServerSocket server = new ServerSocket();

    return server;
}

下面几种方式创建ServerSocket对象时,在创建时就bind到指定的端口,不能做一些初始化配置

// 创建绑定到指定端口的服务器套接字
// 等待连接的最大队列长度设置为50 ,如果连接在队列已满时到达,则连接被拒绝
ServerSocket server = new ServerSocket(PORT);

// 创建服务器套接字并将其绑定到指定的本地端口号,同时并指定了积压
// 等待连接的最大队列长度设置为backlog ,如果连接在队列已满时到达,则连接被拒绝
ServerSocket server = new ServerSocket(PORT, 50);

// 创建一个具有指定端口的服务器,侦听backlog和本地IP地址绑定
ServerSocket server = new ServerSocket(PORT, 50, Inet4Address.getLocalHost());

最后在创建ServerSocket并设置配置后,bind指定的端口

private static final int PORT = 20001;
public static void main(String[] args) throws IOException {

    ServerSocket server = createServerSocket();

    configServerSocket(server);

    //将 ServerSocket绑定到特定地址(IP地址和端口号)
    server.bind(new InetSocketAddress(Inet4Address.getLocalHost(), PORT));

}
2. ServerSocket初始化配置

在设置ServerSocket一些初始化配置时,需要在bind之前才能有效。

private static void configServerSocket(ServerSocket server) throws SocketException {
    // 当TCP连接关闭时,连接可能会在连接关闭后一段时间内保持在超时状态(通常称为TIME_WAIT状态或2MSL等待状态)
    // 如果在套接字地址或端口的超时状态中存在连接,则可能无法将套接字绑定到所需的SocketAddress
    // 设置为true,套接字bind(SocketAddress)允许在上一个连接处于超时状态时绑定套接字
    server.setReuseAddress(true);

    //设置套接字接收缓冲区的大小
    // 注意:在ServerSocket在绑定到本地地址之前调用
    // 也就是意味着必须使用无参数构造函数创建ServerSocket,然后调用setReceiveBufferSize()
    server.setReceiveBufferSize(64 * 1024 * 1024);

    // 设置读取超时时间,单位:毫秒。timeout=0时,无限超时;timeout>0时,与此ServerSocket的accept()调用将仅阻止此时间.
    // 如果超时超时,则引发java.net.SocketTimeoutException
    //server.setSoTimeout(2000);

    //设置性能参数:短链接,延迟,带宽的相对重要性
    server.setPerformancePreferences(1, 1, 0);
}

5.2 基本数据传输

在使用Socket的输出流,传输基本数据类型时,如int类型。

我们先来看使用Socket传输int类型数据,例如:传输int类型10

// 客户端
private static void todo_client(Socket socket) throws IOException {
    OutputStream os = socket.getOutputStream();

    InputStream is = socket.getInputStream();

    os.write(10);

    // 释放资源
    socket.close();
}

// 服务器
private void todo_server(Socket socket) throws IOException {
    // 获取网络输入流
    InputStream is = socket.getInputStream();

    byte[] buffer = new byte[1024];
    int len = is.read(buffer);

    System.out.println("Server-> len: " + len + " data: " + new String(buffer, 0, len));
}

我们发现打印输出的log不是我们期望的,数字10没有输出

> Task :TcpServer.main()
Server-> len: 1 data:

当我们在传输int类型10时,调用下面方法,客户端将int类型转为byte数组。

public static byte[] intToByteArray(int a) {
    return new byte[]{
            (byte) ((a >> 24) & 0xFF),
            (byte) ((a >> 16) & 0xFF),
            (byte) ((a >> 8) & 0xFF),
            (byte) (a & 0xFF)
    };
}

在服务端接收时,调用下面方法,客户端将byte数组转为int类型。

public int byteArrayToInt(byte[] b) {
    return b[3] & 0xFF |
            (b[2] & 0xFF) << 8 |
            (b[1] & 0xFF) << 16 |
            (b[0] & 0xFF) << 24;
}

打印输出的log输出我们期望的值

> Task :TcpServer.main()
Server-> len: 4 data: 10

在JDK java.nio包中,为我们提供了更方面的类ByteBuffer,一个字节缓存区。缓冲区的索引不是以字节为单位,而是根据其值的类型特定大小,如int类型大小为4,long类型大小为8。更重要是缓冲区更高效。

客户端:使用ByteBuffer的wrap方法将字节数组封装到缓冲区中,然后put到缓存区中。如int类型,将int值的四个字节写入当前位置的缓冲区

private static void todo_client(Socket socket) throws IOException {
    OutputStream os = socket.getOutputStream();

    InputStream is = socket.getInputStream();

    byte[]  buffer = new byte[256];
    // 将一个字节数组包装到缓冲区中。
    ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);

    // byte
    byte b = 126;
    byteBuffer.put(b);

    // char
    char c = 'a';
    byteBuffer.putChar(c);

    // int
    int i = 1223344;
    byteBuffer.putInt(i);

    //bool
    boolean bool = true;
    byteBuffer.put(bool ? (byte) 1 : (byte) 0);

    // long
    long l = 1287655778990L;
    byteBuffer.putLong(l);

    //float
    float f = 3.1345f;
    byteBuffer.putFloat(f);

    // double
    double d = 12223.0232199761;
    byteBuffer.putDouble(d);

    // String
    String str = "hello, 你好啊!";
    byteBuffer.put(str.getBytes());

    os.write(buffer, 0, byteBuffer.position() + 1);

    // 释放资源
    socket.close();
}

服务端:使用ByteBuffer的wrap方法将字节数组封装到缓冲区中,然后从缓存区中读取值,如int类型值,在该缓冲区的当前位置读取接下来的四个字节

private void todo_server(Socket socket) throws IOException {
    // 获取网络输入流
    InputStream is = socket.getInputStream();

    byte[] buffer = new byte[256];

    int len = is.read(buffer);

    ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, 0, len);

    //  byte
    byte b = byteBuffer.get();

    // char
    char c = byteBuffer.getChar();

    // int
    int i = byteBuffer.getInt();

    // boolean
    boolean bool = byteBuffer.get() == 1;

    //long
    long l = byteBuffer.getLong();

    // float
    float f = byteBuffer.getFloat();

    // double
    double d = byteBuffer.getDouble();

    int pos = byteBuffer.position();
    String str = new String(buffer, pos, len - pos - 1);


    System.out.println("Server-> len: " + len + "\n"
            +  " b: " + b + "\n"
            +  " c: " + c + "\n"
            +  " i: " + i + "\n"
            +  " bool: " + bool + "\n"
            +  " l: " + l + "\n"
            +  " f: " + f + "\n"
            +  " d: " + d + "\n"
            +  " str: " + str + "\n"
    );
}

从输出的log可以看出,所有基本数据类型的输出都正确:

Server-> len: 46
 b: 126
 c: a
 i: 1223344
 bool: true
 l: 1287655778990
 f: 3.1345
 d: 12223.0232199761
 str: hello, 你好啊!

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)


小兵兵同学
56 声望23 粉丝

Android技术分享平台,每个工作日都有优质技术文章分享。从技术角度,分享生活工作的点滴。