博客主页java
TCP(Transmission Control Protocol)是传输控制协议,一种面向链接的,可靠的,基于字节流的传输层通讯协议。算法
TCP 通讯同UDP通讯同样,都可以实现两台计算机之间的通讯,通讯的两端都须要建立Socket对象。segmentfault
区别在于:数组
在JDK中,提供了两个类用于实现TCP通讯程序缓存
JDK中java.net包中提供ServerSocket类,该类的实例对象能够实现一个服务器段的程序。服务器
ServerSocket(int port) 建立绑定到指定端口的服务器套接字。
使用该构造方法在建立ServerSocket对象时,就能够将其绑定到一个指定的端口号上(参数port就是端口号)网络
Socket accept() 侦听要链接到此套接字并接受它。该方法将阻塞直到创建链接。 InetAddress getInetAddress() 返回此服务器套接字的本地地址。若是套接字被绑定在closed以前,则该方法将在套接字关闭后继续返回本地地址。
ServerSocket对象负责监听某台计算机的某个端口号,在建立ServerSocket对象后,须要继续调用该对象的accept()方法,接收来自客户端的请求。当执行了accept()方法以后,服务器端程序会发生阻塞,直到客户端发出链接请求,accept()方法才会返回一个Scoket对象用于和客户端实现通讯,程序才能继续向下执行.socket
JDK提供了一个Socket类,用于实现TCP客户端程序。tcp
Socket(String host, int port) 建立流套接字并将其链接到指定主机上的指定端口号。
使用该构造方法在建立Socket对象时,会根据参数去链接在指定地址和端口上运行的服务器程序,其中参数host接收的是一个字符串类型的IP地址。函数
Socket(InetAddress address, int port) 建立流套接字并将其链接到指定IP地址的指定端口号。 若是指定的主机是null ,则至关于指定地址为InetAddress.getByName (null) 。 换句话说,它至关于指定回送接口的地址。
该方法在使用上与第二个构造方法相似,参数address用于接收一个InetAddress类型的对象,该对象用于封装一个IP地址。
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流的形式进行交互的,从而实现通讯。
了解了Socket 和 ServerSocket这两个类的基本用法,经过下面简单的TCP加深理解。
注意:若是先启动客户端,抛出java.net.ConnectException: Connection refused (Connection refused)异常
服务端实现:
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->关闭"); } }
服务端实现:
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)); // ... } }
在实际项目实操中,建立客户端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 );
在设置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); }
一般在建立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)); }
在设置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); }
在使用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, 你好啊!
若是个人文章对您有帮助,不妨点个赞鼓励一下(^_^)