网络编程使计算机不再是一个孤立的个体,而是一个互联的整体,通过网络编程,可实现主机各进程间的数据传输。网络编程的核心就是套接字socket。socket是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。
socket主要分为三类:流套接字、数据报套接字、原始套接字。我们这里主要介绍前两种:
流套接字:使用传输层TCP协议,提供面向连接、可靠的数据传输,面向字节流,有接收缓冲区和发送缓冲区,传输数据大小无限制。
数据报套接字:使用传输层UDP协议,提供无连接、不可靠传输,面向数据报,只有接收缓冲区,传输大小受限,一次最多传输64K。
下面我们来看下Java中是如何使用socket来进行网络编程的。
数据报套接字
Java数据报套接字编程有两个核心的类,DatagramSocket和DatagramPacket。
两个核心类
DatagramSocket用于创建一个数据报套接字,用于发送和接收数据报。主要的构造方法如下:
| 方法名 |
说明 |
| DatagramSocket() |
创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端) |
| DatagramSocket(int port) |
创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用 于服务端) |
常用方法如下:
| 方法名 |
说明 |
| void receive(DatagramPacket p) |
从此套接字接收数据报(如果没有接收到数据报,会阻塞等待) |
| void send(DatagramPacket p) |
从此套接字发送数据报包(不会阻塞等待,直接发送) |
| void close() |
关闭此数据报套接字 |
方法内的参数类型,就是我们要介绍的另一个核心类:DatagramPacket,该类表示数据报包,用于进程间的发送和接收。主要构造方法如下:
| 方法名 |
说明 |
| DatagramPacket(byte[] buf, int length) |
构造一个数据报包,用来接收,接收的数据保存在字节数组buf中,接收指定长度length |
| DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) |
构造一个数据报包,用来发送,发送的数据为字节数组buf,从offset到长度length,指明目标主机地址address,即IP和端口号 |
常用方法如下:
| 方法名 |
说明 |
| InetAddress getAddress() |
获取该数据报发送或接收数据报的计算机的IP地址。 |
| getSocketAddress() |
获取该数据包发送到或正在从其发送的远程主机的SocketAddress(通常为IP地址+端口号)。 |
| int getPort() |
获取该数据报发送或接收数据报的端口号 |
| byte[] getData() |
获取数据报中的数据 |
| int getLength() |
返回要发送的数据的长度或接收到的数据的长度。 |
服务器端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public class UdpServer {
private static DatagramSocket socket = null;
public UdpServer(int port) throws SocketException { socket = new DatagramSocket(port); }
public void start(){ System.out.println("服务器已连接"); try { while(true){ DatagramPacket reqPacket = new DatagramPacket(new byte[1024],1024); socket.receive(reqPacket); String req = new String(reqPacket.getData(),0,reqPacket.getLength(),"utf8"); String resp = process(req); DatagramPacket respPacket = new DatagramPacket(resp.getBytes(),resp.getBytes().length,reqPacket.getSocketAddress()); socket.send(respPacket); System.out.printf("[%s:%d] req: %s, resp: %s\n", reqPacket.getAddress().toString(), reqPacket.getPort(), req, resp); } } catch (IOException e) { e.printStackTrace(); } }
private String process(String req) { return req; }
public static void main(String[] args) throws SocketException { UdpServer server = new UdpServer(9090); server.start(); } }
|
代码中★处要尤为注意:
构造请求字符串的第三个参数:请求字符串长度,是接收到的数据报的长度,reqPacket.getLength()。一定不要写成reqPacket.getData().length(),这个是字节数组的长度,该代码为1024。
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class UdpClient {
private final int port; private final String ip; private DatagramSocket socket = null;
public UdpClient(String ip,int port) throws SocketException { this.socket = new DatagramSocket(); this.ip = ip; this.port = port; } public void start(){
try { while(true){ System.out.println("请输入请求:"); Scanner scanner = new Scanner(System.in); String req = scanner.nextLine(); DatagramPacket reqPacket = new DatagramPacket(req.getBytes(),req.getBytes().length,InetAddress.getByName(ip),port); socket.send(reqPacket); DatagramPacket respPacket = new DatagramPacket(new byte[1024],1024); socket.receive(respPacket); String resp = new String(respPacket.getData(),0,respPacket.getLength()); System.out.println("resp:"+resp); } } catch (IOException e) { e.printStackTrace(); }
}
public static void main(String[] args) throws SocketException { UdpClient client = new UdpClient("127.0.0.1",9090); client.start(); }
|
代码中★处:
构造发送数据包时要传入目的主机的地址,InetAddress.getByName(ip),port分别为主机的ip地址和端口号,除了这样,还可以通过创建socketAddress类来指定主机地址。
1 2
| SocketAddress socketAddress = new InetSocketAddress(ip,port); DatagramPacket reqPacket = new DatagramPacket(req.getBytes(),req.getBytes().length,socketAddress);
|
流套接字
Java流套接字编程也有两个核心的类,ServerSocket和Socket。
两个核心类
ServerSockert通常用于创建一个服务器端的流套接字,常用构造方法如下:
| 方法名 |
说明 |
| ServerSocket(int port) |
创建一个服务端流套接字Socket,并绑定到指定端口 |
常用方法如下:
| 方法名 |
说明 |
| Socket accept() |
监听要连接此客户端的套接字,有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
| void close() |
关闭此套接字 |
Socket通常用于常见一个客户端的流套接字,常用构造方法如下:
| 方法名 |
说明 |
| Socket(String host, int port) |
创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
常用方法如下:
| 方法名 |
说明 |
| InetAddress getInetAddress() |
返回套接字所连接的地址 |
| InputStream getInputStream() |
返回此套接字的输入流 |
| OutputStream getOutputStream() |
返回此套接字的输出流 |
服务器端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public class TcpServer { ServerSocket server = null;
public TcpServer(int port) throws IOException { server = new ServerSocket(port); }
public void start() throws IOException { while(true){ Socket client = server.accept(); processConnection(client); } }
public void processConnection(Socket client) throws IOException { System.out.printf("[%s,%d]客户端建立连接\n",client.getInetAddress(),client.getPort()); try(InputStream inputStream = client.getInputStream()){ try(OutputStream outputStream = client.getOutputStream()){ while(true){ Scanner scanner = new Scanner(inputStream); if(!scanner.hasNext()){ System.out.printf("[%s,%d]客户端断开连接",client.getInetAddress(),client.getPort()); break; } String req = scanner.nextLine(); String resp = process(req); PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(resp); printWriter.flush(); System.out.printf("req:%s,resp:%s\n",req,resp); } } }finally { client.close(); } }
private String process(String req) { return req; }
public static void main(String[] args) throws IOException { TcpServer server = new TcpServer(9090); server.start(); } }
|
★处要尤为注意:
-
调用的是println方法,不是print也不是write。println方法会为写入的数据后面添加上换行符,而print和write不会。客户端接收数据时是以换行符为结束来获取数据。如果找不到换行符,将一直不能接收响应,造成阻塞。同理,客户端发送请求时也要使用println方法,否则服务器也会接收不到请求而一直阻塞。
-
大家可能会想,为什么这里的client要关闭连接,而server和之前的数据报套接字都不需要关闭连接。关闭连接的前提是不再使用了,对于UDP和ServerSocket,它们是贯穿程序始终的,而这里的client,也就是Socket,每个连接都有一个Socket,断开连接也就不再需要这个Socket了,需要进行释放。
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class TcpClient { private int port; private String ip; Socket client = null;
public TcpClient(String ip,int port) throws IOException { this.port = port; this.ip = ip; this.client = new Socket(ip,port); }
public void start(){
try(InputStream inputStream = client.getInputStream()) { try(OutputStream outputStream = client.getOutputStream()){
while(true){ Scanner scanner = new Scanner(System.in); String req = scanner.next(); PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(req); printWriter.flush(); Scanner respScan = new Scanner(inputStream); String resp = respScan.next(); System.out.println("resp:"+resp); } } } catch (IOException e) { e.printStackTrace(); }
}
public static void main(String[] args) throws IOException { TcpClient client = new TcpClient("127.0.0.1",9090); client.start(); } }
|
问题
流套接字的客户端服务器通信还存在一个问题,只能满足一个客户端的通信。其他客户端尝试与服务器连接时,会发生阻塞。观察服务器代码中下面这段代码:
1 2 3 4 5 6 7
| public void start() throws IOException { while(true){ Socket client = server.accept(); processConnection(client); } }
|
客户端1与服务器建立连接后,服务器线程执行processConnection代码中的操作,代码中存在while循环,不断地接收客户端1发来的请求,构造响应,返回响应……此时客户端2想要与服务器通信,而服务器线程正在processConnection的while循环中等待客户端1的请求,导致服务器不能与客户端2建立连接。
TCP每次都需要建立连接,只能支持一对一,也就是端到端的通信。就像打电话一样,同一时间只能接听一个电话,当正在通话时,别的电话是打不进来的。而UDP是无连接的,因此支持一对一、一对多、多对多交互通信。所以我们在数据报套接字时没有出现该问题。
解决办法也很简单,因为processConnection中的循环未结束导致服务器不能执行accept操作,所以单独创建一个新的线程来完成processConnection操作即可。我们可以用多线程或线程池来解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public void start() throws IOException { while(true){ Socket client = server.accept(); Thread t = new Thread(()->{ try { processConnection(client); } catch (IOException e) { e.printStackTrace(); } }); t.start();
}
|
其他代码不变,只需改动start方法内的代码,为processConnection操作创建新的线程。