前言:
如今,网络编程已然成为了一个后端开发工程师需要具备的核心技能之一。因此,该博客力求提供最简单、通俗的描述方式,来描绘网络编程中常见的知识点,同时附带代码示例,后期会加上具体的抓包分析,实际项目、框架案例,希望可以和大家共同探索网络世界。
什么是socket?
socket在网络编程中的位置
- TCP是面向连接的、UDP是面向无连接的,所谓面向连接,指的就是通讯双方会维护一组状态,保证连接的可靠性。
- TCP的数据包结构相对复杂,而UDP则相对简单。无论是TCP还是UDP,他们的包头上都应该有端口号和目标端口号,TCP的包头上有序号(顺序),确认序号(不丢包)、窗口大小(流量控制 & 拥塞控制)、状态码(FIN ACK SYN)等。TCP和UDP的包头如下图。
- TCP数据传输方式是基于数据流的,而UDP则是基于数据报。
udp包头
tcp包头
如何使用socket:
代码(TCP):
服务器:
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; /** * 服务器. * * @author jialin.li * @date 2019-12-10 19:45 */ public class TcpServer { public static void main(String[] args) throws IOException { int port = 8099; ServerSocket connectionSocket = new ServerSocket(port); // 监听端口,accept为阻塞方法 System.out.println("Get socket successfully, wait for request..."); Socket communicationSocket = connectionSocket.accept(); // 获取输入流,读取数据 InputStream inputStream = communicationSocket.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String message; while ((message = bufferedReader.readLine()) != null) { System.out.printf("get message from client : %s",message); } communicationSocket.shutdownInput(); // 获取输出流,返回结果 OutputStream outputStream = communicationSocket.getOutputStream(); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter); bufferedWriter.write("I got your message and the communication is over."); bufferedWriter.flush(); communicationSocket.shutdownOutput(); // 关闭资源 bufferedWriter.close(); bufferedReader.close(); } }
客户端:
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.Socket; /** * 客户端. * * @author jialin.li * @date 2019-12-10 22:30 */ public class TcpClient { public static void main(String[] args) throws IOException { // 指定ip 端口,创建socket String host = "127.0.0.1"; int port = 8099; Socket communicationSocket = new Socket(host, port); // 获取输出流,写入数据 OutputStream outputStream = communicationSocket.getOutputStream(); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter); bufferedWriter.write("hello world"); bufferedWriter.flush(); communicationSocket.shutdownOutput(); // 获取输入流,读取服务器返回信息 InputStream inputStream = communicationSocket.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String message; while ((message = bufferedReader.readLine()) != null) { System.out.printf("get message from server : %s", message); } communicationSocket.shutdownInput(); // 关闭资源 bufferedWriter.close(); bufferedReader.close(); } }
执行结果:
server:
Get socket successfully, wait for request...
get message from client : hello world
client:
get message from server : I got your message and the communication is over.
outputStream.close与shutdownOutput的区别:
这里只对outputStream进行分析,inputStream与之相同。
可以看出,上述代码,我们在执行完输入/输出动作之后,会调用一个shutdownInput/shutdownOutput方法,这个方法有什么作用呢?可不可以用close方法替代?
首先,我们阅读jdk中关于该方法的doc注释:
Disables the output stream for this socket.
For a TCP socket, any previously written data will be sent
followed by TCP's normal connection termination sequence.
If you write to a socket output stream after invoking
shutdownOutput() on the socket, the stream will throw
an IOException.
大体意思是说,该方法会禁用socket的输出流,对于基于TCP协议的Socket,任何在方法执行之前发送数据,都可以被正常发送,如果在这个方法执行后发送数据,就会抛出一个IO异常。通过该方法关闭流,Socket连接不会收到影响,但是如果我们直接使用close方法关闭流,那么Socket连接也会随之关闭。接下来我们来测试一下,在Server中用inputStream.close来代替socket.shutdownInput方法。
结果是由于inputStream.close提前关闭了socket,导致服务器在输出数据的时,socket.getOutputStream方法抛出异常:java.net.SocketException: Socket is closed
为什么我们每次调用BufferedWrite的write方法,都要调用flush方法:
这个问题不属于网络编程的范畴,但却是我们在写socket程序时,很容易犯的一个错误。bufferedWrite是字符缓存流,它的原理其实很简单,在内存中设置一个缓存区,将原本逐个发送的字符缓存起来,批量发送(有很多框架也采用了这种思想,比如kafka的批量发送),flush方法是手动的将我们缓存区中的数据刷出。缓存区中的数据,将会在流关闭之前,进行flush。但是由于我们在发送数据之后,调用了shutdownOutput方法,导致最后close的时候,没办法将缓存区中的数据flush,因此会抛出一个写入失败的异常:java.net.SocketException: Broken pipe (Write failed)。
UDP是基于数据报的,因此不需要每对连接都建立一组socket,而是只要有一个socket,就能与多个客户端通讯,所以只需要通过创建数据报,然后通过socket发送即可,具体步骤如下:
代码(UDP):
服务器:
import java.net.DatagramPacket; import java.io.IOException; import java.net.DatagramSocket; /** * 服务器. * * @author jialin.li * @date 2019-12-10 19:45 */ public class UdpServer { public static void main(String[] args) throws IOException { // 监听端口,阻塞方法 int port = 8099; DatagramSocket socket = new DatagramSocket(port); // 创建数据报,用于接收客户端发送的数据 byte[] data = new byte[1024]; DatagramPacket packet = new DatagramPacket(data, data.length); // 接收客户端发送的数据 System.out.println("Get socket successfully, wait for request..."); socket.receive(packet); String message = new String(data, 0, packet.getLength()); System.out.printf("get message from client : %s", message); // 向客户端发送数据 byte[] data2 = "I got your message and the communication is over.".getBytes(); DatagramPacket packet2 = new DatagramPacket(data2, data2.length, packet.getAddress(), packet.getPort()); socket.send(packet2); socket.close(); } }
客户端:
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /** * 客户端. * * @author jialin.li * @date 2019-12-11 11:32 */ public class UdpClient { public static void main(String[] args) throws IOException { // 封装数据报:ip、端口、数据 InetAddress ip = InetAddress.getByName("127.0.0.1"); int port = 8099; byte[] data = "hello world".getBytes(); DatagramPacket packet = new DatagramPacket(data, data.length, ip, port); // 创建socket,发送数据报 DatagramSocket socket = new DatagramSocket(); socket.send(packet); // 读取服务器返回信息 byte[] data2 = new byte[1024]; DatagramPacket packet2 = new DatagramPacket(data2, data2.length); socket.receive(packet2); // 读取数据 String message = new String(data2, 0, packet2.getLength()); System.out.printf("get message from server : %s", message); //关闭资源 socket.close(); } }
执行结果:
server:
Get socket successfully, wait for request...
get message from client : hello world
client:
get message from server : I got your message and the communication is over.