1. TCP
TCP(Transmission Control Protocol)是传输控制协议,一种面向连接的,可靠的,基于字节流的传输层通信协议。
TCP 通信同UDP通信一样,都能够实现两台计算机之间的通信,通信的两端都需要创建Socket对象。
区别在于:
- UDP中只有发送端和接收端,不区分客户端与服务端,计算机之间可以任意地发送数据
- TCP通信严格区分客户端与服务器端,在通信时,必须先又客户端去连接服务器端才能通信,服务器端不能主动连接客户端,并且,服务器端需要先启动,等待客户端的连接
在JDK中,提供了两个类用于实现TCP通信程序
- 客户端:java.net.Socket类表示。创建Socket对象,向服务器端发出连接请求,服务器端响应请求,两者建立连接才能开始通信。
- 服务端:java.net.ServerSocket类表示。创建ServerSocket对象,相当于开启了一个服务,等待客户端连接。
2. ServerSocket
JDK中java.net包中提供ServerSocket类,该类的实例对象可以实现一个服务器段的程序。
- ServerSocket类提供了多种构造方法:
ServerSocket(int port)
创建绑定到指定端口的服务器套接字。
使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上(参数port就是端口号)
- ServerSocket的常用方法:
Socket accept()
侦听要连接到此套接字并接受它。该方法将阻塞直到建立连接。
InetAddress getInetAddress()
返回此服务器套接字的本地地址。如果套接字被绑定在closed之前,则该方法将在套接字关闭后继续返回本地地址。
ServerSocket对象负责监听某台计算机的某个端口号,在创建ServerSocket对象后,需要继续调用该对象的accept()方法,接收来自客户端的请求。当执行了accept()方法之后,服务器端程序会发生阻塞,直到客户端发出连接请求,accept()方法才会返回一个Scoket对象用于和客户端实现通信,程序才能继续向下执行.
3. Socket
JDK提供了一个Socket类,用于实现TCP客户端程序。
- Socket类同样提供了多种构造方法
Socket(String host, int port)
创建流套接字并将其连接到指定主机上的指定端口号。
使用该构造方法在创建Socket对象时,会根据参数去连接在指定地址和端口上运行的服务器程序,其中参数host接收的是一个字符串类型的IP地址。
Socket(InetAddress address, int port)
创建流套接字并将其连接到指定IP地址的指定端口号。
如果指定的主机是null ,则相当于指定地址为InetAddress.getByName (null) 。 换句话说,它相当于指定回送接口的地址。
该方法在使用上与第二个构造方法类似,参数address用于接收一个InetAddress类型的对象,该对象用于封装一个IP地址。
- 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, 你好啊!
如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。