Socket网络编程(三):Socket TCP快速入门

博客主页java

1. TCP

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

TCP 通讯同UDP通讯同样,都可以实现两台计算机之间的通讯,通讯的两端都须要建立Socket对象。segmentfault

区别在于:数组

  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对象用于和客户端实现通讯,程序才能继续向下执行.socket

3. Socket

JDK提供了一个Socket类,用于实现TCP客户端程序。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, 你好啊!

若是个人文章对您有帮助,不妨点个赞鼓励一下(^_^)

相关文章
相关标签/搜索