TCP
TCP 协议提供面向连接的服务,通过它建立的是可靠地连接。Java 为 TCP 协议提供了两个类:Socke 类和 ServerSocket 类。一个 Socket 实例代表了 TCP 连接的一个客户端,而一个 ServerSocket 实例代表了 TCP 连接的一个服务器端,一般在 TCP Socket 编程中,客户端有多个,而服务器端只有一个,客户端 TCP 向服务器端 TCP 发送连接请求,服务器端的 ServerSocket 实例则监听来自客户端的 TCP 连接请求,并为每个请求创建新的 Socket 实例,由于服务端在调用 accept()等待客户端的连接请求时会阻塞,直到收到客户端发送的连接请求才会继续往下执行代码,因此要为每个 Socket 连接开启一个线程。服务器端要同时处理 ServerSocket 实例和 Socket 实例,而客户端只需要使用 Socket 实例。另外,每个 Socket 实例会关联一个 InputStream 和 OutputStream 对象,我们通过将字节写入套接字的 OutputStream 来发送数据,并通过从 InputStream 来接收数据。
From:极客wiki
创建服务器
- 通过
ServerSocket
创建服务套接字,并监听本地端口(以22333端口为例)
//:TCPServer.java
ServerSocket serverSocket = new ServerSocket(22333);
while(true){
Socket conn = serverSocket.accept();
onClientArrived(conn);
}
//deal with the request connection
public void abstract onClientArrived(Socket conn);
在新的连接到达前ServerSocket#accept()
将处于阻塞状态。当每个连接到达时accept
函数都将返回一个Socket
实例;
2) 在onClientArrived
中处理新连接
@Override
public void onConnArrived(Socket conn) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String tmpLine = null;
while ((tmpLine = reader.readLine()) != null){
System.out.println(tmpLine);
}
reader.close();
conn.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Reader#readLine()
为一个阻塞读取过程,因此需要在客户端使用(启用auto flush)PrintWriter
的println
等含有自动newLine
的 函数与之配合使用。这里为了简便在读取完成后,直接关闭连接,如果想进行持续会话,则可根据conn
的内容来控制时候结束会话,并且需要保证客户端并没有主动关闭连接。另外,通常当ServerSocket#accept()
返回新Socket
对象后,应该为该对象创建一个任务,并将该任务交由线程池处理。
创建客户端
- 客户端套接字
连接服务22333端口(Dst Port),同时在TCP协议中系统会随机产生一个本地端口(Local Port)供通讯使用。
Socket client = new Socket("127.0.0.1", 22333);
- 向套接字中写入数据
//使用PrintWriter向套接字的OutputStream中写入数据
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(client.getOutputStream()), true);
printWriter.println("hello ! this msg is send by client");
printWriter.close();
client.close();
此处在写入完成直接关闭连接。若希望持续会话,则可在服务端的连接没有关闭的情况下,此处可以通过对Socket
输入流的read
操作和输出流的write
操作实现持续会话
3) 完整代码
//:TCPTest.java
public class TCPTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
TCPServer tcpServer = new TCPServer() {
@Override
public void onConnArrived(Socket conn) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String tmpLine = null;
while ((tmpLine = reader.readLine()) != null){
System.out.println("receive msg from client" + tmpLine);
}
reader.close();
conn.close();
System.out.println("conn shutdown");
} catch (IOException e) {
e.printStackTrace();
}
}
};
tcpServer.startServer();
}
}).start();
try {
Socket socket = new Socket("127.0.0.1",TCPServer.PORT);
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
printWriter.println("hello ! this msg is send by client");
printWriter.close();
socket.close();
System.out.println("client close");
} catch (IOException e) {
e.printStackTrace();
}
}
}
//:TCPServer.java
public abstract class TCPServer {
public static final int PORT = 22333;
private static ServerSocket serverSocket;
private volatile boolean ifServerIsRunning = false;
public TCPServer(){
synchronized (TCPServer.class){
if (serverSocket == null){
try {
serverSocket = new ServerSocket(PORT);
}catch (IOException e){
e.printStackTrace();
}
}
}
}
public void startServer(){
if (!ifServerIsRunning){
ifServerIsRunning = true;
while (true){
try {
Socket conn = serverSocket.accept();
onConnArrived(conn);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public abstract void onConnArrived(Socket conn);
}
由以上的程序可以看出,Socket
就是进程与进程之间的一种通信方式,一个位于服务端,一个位于客户端。Socket
并不代表运输层,它只是一个网络编程API,当选用Socket
时,代表我们希望使用TCP
协议通信,当选用DatagramSocket
时代表我们希望使用UDP
协议进行通信。
UDP
Java 通过 DatagramPacket 类和 DatagramSocket 类来使用 UDP 套接字,客户端和服务器端都通过DatagramSocket 的 send()方法和 receive()方法来发送和接收数据,用 DatagramPacket 来包装需要发送或者接收到的数据。发送信息时,Java 创建一个包含待发送信息的 DatagramPacket 实例,并将其作为参数传递给DatagramSocket实例的send()方法;接收信息时,Java 程序首先创建一个 DatagramPacket 实例,该实例预先分配了一些空间,并将接收到的信息存放在该空间中,然后把该实例作为参数传递给 DatagramSocket 实例的 receive()方法。在创建 DatagramPacket 实例时,要注意:如果该实例用来包装待接收的数据,则不指定数据来源的远程主机和端口,只需指定一个缓存数据的 byte 数组即可(在调用 receive()方法接收到数据后,源地址和端口等信息会自动包含在 DatagramPacket 实例中),而如果该实例用来包装待发送的数据,则要指定要发送到的目的主机和端口。
From:极客wiki
小结
- 在读取未关闭的socket的过程中,任何的
read()
、readline()
操作都是一个阻塞过程直到读取到内容或者socket关闭,可对比new Scanner(System.in)
的类next
操作。 ServerSocket
的accept()
操作是同样是一个阻塞过程,直到收到一个连接请求,每次客户端成功发起请求后,accept()
都返回一个Socket
对象,这个Socket
对象将负责与发起请求的本次客户端进行通讯。Reader
的readLine()
需要使用'\n'
作为此次读取结束标记,否则将一直阻塞在读取状态。- “一切皆文件”