• 网络编程套接字



     目录

    🥬网络编程

    🥬Socket套接字

    🥬UDP数据报套接字编程

     🥬TCP流套接字编程

    🥬小结


    🥬网络编程

    网路编程就是通过代码来控制,让两个主机的进程之间能够进行数据交互。操作系统把网络编程的一些相关操作,封装起来了,提供了一组API(接口)来调用。

    🥬Socket套接字

    Socket (操作系统提供的网络编程的API的统称):套接字,是由系统提供用于网络通信的技术,是基于 TCP/IP 协议的网络通信的基本操作单元。基于 Socket 套接字的网络程序开发就是网络编程,进行网络编程的核心就是通过代码操作网卡这个硬件设备,操作系统对网卡进行了抽象,进程去操作网卡的时候,就会打开一个"socket文件",通过读写这个socket文件,就能读写网卡了。
    操作系统提供的Socke API(属于传输层)主要有两类:

    1、流套接字(底层使用TCP协议)

    2、数据报套接字(底层使用UDP协议)

    TCP特点:

    1、有连接

    2、可靠传输

    3、面向字节流

    4、全双工

    UDP特点:

    1、无连接

    2、不可靠传输

    3、面向数据报

    4、全双工

    有连接就相当于我们打电话,A打给B,B接起来了,这时候A和B才能进行通信。

    无连接就相当于我们发信息,A发过去给B了,B收到了,但是我们也不能确定B是否有看到消息。

    可靠传输就是说发送方能够知道对方收到了,不可靠传输就是说发送方不知道对方是不是收到了。

    面向字节流:我们为了发送100个字节,可以一次发送1个字节,重复100次,也可以一次发送10个字节,重复10次……非常灵活完成发送,接收也是如此

    面向数据报:以一个一个数据报为基本单位(每个数据报多大,不同协议约定的不一样),发送的时候一次至少发送一个数据报(如果尝试一次发一个半,实际只能发出去一个),接收的时候一次至少接收一个数据报(如果尝试接收半个,剩下半个就没有了)

    全双工:双向通信表示A和B可以同时向对方发送接收数据;半双工:单向通信,要么A给B发,要么B给A发,不能同时发。

    🥬UDP数据报套接字编程

    UDP Socket有两个核心的类:

    1、DatagramSocket 描述一个Socket对象。

    receive:面向数据报,接收数据,如果没有数据过来,receive就会阻塞等待,如果有数据,receive就能返回一个DatagramPacket对象。

    send:发送数据,以DatagramPacket为单位进行发送

    2、DatagramPacket 描述一个UDP数据报。

    例子1:基于UDP的socket来写一个比较简单的回显(Echo)程序回显客户端+回显服务器,回显就是A说啥B就回应啥。

    正常的客户端/服务器通信流程:

    代码示例:

    UdpEchoServer 服务器

    1. //服务器
    2. public class UdpEchoServer {
    3. private DatagramSocket socket=null;
    4. //port表示端口号
    5. //服务器在启动的时候,需要关联上一个端口号
    6. //收到数据的时候,就会根据这个端口号来决定把这个数据交给哪个进程
    7. //虽然这里的port写的是int类型,但是实际上端口号是一个两个字节的无符号整数
    8. //范围是0-65535
    9. public UdpEchoServer(int port) throws SocketException{
    10. socket=new DatagramSocket(port);
    11. }
    12. //通过这个方法启动服务器
    13. public void start() throws IOException {
    14. System.out.println("服务器启动");
    15. //服务器一般都是持续运行的(7*24)
    16. while (true) {
    17. //1、读取请求
    18. //由于当前服务器不知道客户端什么时候发来请求,所以receive方法会阻塞
    19. //如果真的有请求过来了,此时receive就会返回
    20. //参数DatagramPacket 是一个输出型参数,socket中读取到的数据会设置到这个参数的对象中
    21. //DatagramPacket在构造的时候,需要一个指定的缓冲区(就是一段内存空间,通常使用byte[])
    22. DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
    23. //receive得到的结果并没有作为一个返回值,而是一个输出型参数(这个传入的参数未知,需要通过receive这个函数传出来)
    24. //这样子可以让用户给DatagramPacket制定一个缓冲区
    25. socket.receive(requestPacket);
    26. //把requestPacket对象的内容取出来,作为一个字符串
    27. String request=new String(requestPacket.getData(),0,requestPacket.getLength());
    28. //2、根据请求计算响应
    29. String response=process(request);
    30. //3、把响应写回客户端
    31. DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
    32. requestPacket.getSocketAddress());
    33. //此处DatagramPacket中设置的长度,必须是"字节的个数"
    34. //如果直接取response.length()得到的就是字符串长度 也就是"字符个数"
    35. // responsePacket在构造的时候,还得指定这个包要发给谁,发送的目标就是发送请求的那一方
    36. socket.send(responsePacket);
    37. //4、打印
    38. String log=String.format("[%s:%d] req: %s; resp :%s",
    39. requestPacket.getAddress().toString(),//IP地址
    40. requestPacket.getPort(),//端口号
    41. request,response,"hello");
    42. System.out.println(log);
    43. }
    44. }
    45. //根据请求计算响应
    46. //由于此处是回显服务器,所以直接把客户端请求返回即可
    47. private String process(String request) {
    48. return request;
    49. }
    50. public static void main(String[] args) throws IOException {
    51. UdpEchoServer server=new UdpEchoServer(9090);
    52. server.start();
    53. }
    54. }

    UdpEchoClient  客户端

    1. public class UdpEchoClient {
    2. private DatagramSocket socket=null;
    3. private String serverIp;
    4. private int serverPort;
    5. public UdpEchoClient(String serverIp,int serverPort) throws SocketException{
    6. this.serverIp=serverIp;
    7. this.serverPort=serverPort;
    8. //DatagramSocket实例化这里没有传入port,相当于没有指定端口号,系统会自动分配一个空闲的端口号给客户端
    9. this.socket=new DatagramSocket();
    10. }
    11. public void start() throws IOException{
    12. Scanner scanner=new Scanner(System.in);
    13. while(true){
    14. //1、从标准输入读取一个数据
    15. System.out.println("->");
    16. String request=scanner.nextLine();
    17. if(request.equals("exit")){
    18. System.out.println("exit!");
    19. return;
    20. }
    21. //2、把字符串构造成一个UDP请求,并发送数据
    22. //这个DatagramPacket,既要包含具体的数据,又要包含这个数据发给谁
    23. DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
    24. InetAddress.getByName(serverIp),serverPort);
    25. socket.send(requestPacket);
    26. //3、尝试从服务器读取响应
    27. DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
    28. socket.receive(responsePacket);
    29. String response=new String(responsePacket.getData(),0,responsePacket.getLength());
    30. //4、显示这个结果
    31. String log=String.format("req: %s; resp: %s",request,response);
    32. System.out.println(log);
    33. }
    34. }
    35. public static void main(String[] args) throws IOException{
    36. UdpEchoClient client=new UdpEchoClient("127.0.0.1", 9090);
    37. client.start();
    38. }
    39. }

    例子2:基于UDP的socket的翻译程序

    代码示例:

    服务器

    1. public class UdpEchoServer {
    2. private DatagramSocket socket=null;
    3. private HashMap dict=new HashMap<>();
    4. public UdpEchoServer(int port) throws SocketException {
    5. socket=new DatagramSocket(port);
    6. //对hash表的值进行初始化
    7. //根据需求,这个表的数据可以非常多
    8. dict.put("hello","你好");
    9. dict.put("fruit","水果");
    10. dict.put("animal","动物");
    11. }
    12. private String process(String request) {
    13. //利用查表来实现翻译的效果
    14. return dict.getOrDefault(request,"[单词在词典中不存在]");
    15. }
    16. public void start() throws IOException {
    17. System.out.println("服务器启动");
    18. while(true){
    19. //1、读取请求并解析
    20. DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
    21. socket.receive(requestPacket);
    22. String request=new String(requestPacket.getData(),0,requestPacket.getLength());
    23. //2、根据请求计算响应
    24. String response=process(request);
    25. //3、把响应写回客户端
    26. DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
    27. requestPacket.getSocketAddress());
    28. socket.send(responsePacket);
    29. //4、打印
    30. String log=String.format("[%s:%d] req: %s; resp: %s",
    31. requestPacket.getAddress().toString(),
    32. requestPacket.getPort(),
    33. request,response);
    34. System.out.println(log);
    35. }
    36. }
    37. public static void main(String[] args) throws IOException {
    38. UdpEchoServer server=new UdpEchoServer(9090);
    39. server.start();
    40. }
    41. }

    客户端

    1. public class UdpEchoClient {
    2. private DatagramSocket socket=null;
    3. private String serverIp;
    4. private int serverPort;
    5. public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
    6. this.serverIp=serverIp;
    7. this.serverPort=serverPort;
    8. socket=new DatagramSocket();
    9. }
    10. public void start() throws IOException {
    11. Scanner sc=new Scanner(System.in);
    12. while(true){
    13. //1、读取数据
    14. System.out.println("->");
    15. String request=sc.nextLine();
    16. if(request.equals("exit")){
    17. System.out.println("exit!");
    18. return;
    19. }
    20. //2、构造请求并发送给服务器
    21. DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
    22. InetAddress.getByName(serverIp), serverPort);
    23. socket.send(requestPacket);
    24. //3、从服务器中读取响应
    25. DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
    26. socket.receive(responsePacket);
    27. String response=new String(responsePacket.getData(),0,responsePacket.getLength());
    28. //4、打印
    29. String log=String.format("req: %s; resp: %s",request,response);
    30. System.out.println(log);
    31. }
    32. }
    33. public static void main(String[] args) throws IOException{
    34. UdpEchoClient client=new UdpEchoClient("127.0.0.1", 9090);
    35. client.start();
    36. }
    37. }

     🥬TCP流套接字编程

    TCP socket这里用的类是ServerSocket和Socket,这里有个accept特别要注意:

    accept:和TCP中的"有连接"这个特性密切相关,如果A给B打电话,B要接通电话,A和B才能通信,accept就相当于接电话这个动作,客户端尝试建立连接,首先服务器先建立好一切连接,做好准备工作,客户端调用accept,才是真的把这个连接拿到用户代码中。

    注意:

    socket对应到文件了,所以也会有一个close方法,打开一个文件之后,要记得及时关闭!如果不关闭就会出现资源泄露。但是前面写的Udp版本我们没有用close关闭,其实是因为Udp版本的程序不太需要关闭(也不能关闭),UdpServer、UdpClient里面的socket生命周期都是需要跟随整个程序的。如果socket没有关闭,当进程结束的时候,对应的资源也就自然释放了,资源描述的是文件描述符,文件描述符在文件描述符表里面,文件描述符表在PCB里,如果进程没了,对于的PCB就已经销毁了。

    例子:用 TCP socket 写一个回显程序

    代码示例:

    服务器

    1. public class TcpEchoServer {
    2. private ServerSocket listenSocket=null;
    3. public TcpEchoServer(int port) throws IOException {
    4. listenSocket=new ServerSocket(port);
    5. }
    6. public void start() throws IOException {
    7. System.out.println("程序启动");
    8. while(true){
    9. //UDP的服务器进入主循环,就直接尝试receive请求
    10. //但是TCP是有连接的,首先需要做的是建立好连接
    11. //当服务器运行的时候,如果客户端没有建立连接,accept就会阻塞等待
    12. //如果客户端建立连接了,accept就会返回一个socket对象
    13. //进一步的服务器和客户端之间的交互,就交给clientSocket来完成了
    14. Socket clientSocket=listenSocket.accept();
    15. processConnection(clientSocket);
    16. }
    17. }
    18. private void processConnection(Socket clientSocket) throws IOException {
    19. String log=String.format("[%s:%d] 客户端上线!",
    20. clientSocket.getInetAddress().toString(),
    21. clientSocket.getPort());
    22. System.out.println(log);
    23. //TCP socket也是字节流,进行数据读写时跟之前读写文件是类似的,也是使用InputStream和OutputStream
    24. try(InputStream inputStream=clientSocket.getInputStream();
    25. OutputStream outputStream= clientSocket.getOutputStream()){
    26. while(true){
    27. //1、读取请求并解析
    28. Scanner sc=new Scanner(inputStream);
    29. if(!sc.hasNext()) {
    30. log = String.format("[%s:%d] 客户端下线!",
    31. clientSocket.getInetAddress().toString(),
    32. clientSocket.getPort());
    33. System.out.println(log);
    34. break;
    35. }
    36. String request=sc.next();
    37. //2、根据请求计算响应
    38. String response=process(request);
    39. //3、把响应写回客户端
    40. PrintWriter writer=new PrintWriter(outputStream);
    41. writer.println(response);
    42. writer.flush();
    43. log=String.format("[%s:%d] req: %s; resp: %s",
    44. clientSocket.getInetAddress().toString(),clientSocket.getPort(),
    45. request,response);
    46. System.out.println(log);
    47. }
    48. }catch(IOException e){
    49. e.printStackTrace();
    50. }finally {
    51. //当前clientSocket的生命周期并不是跟随整个程序的,而是和连接有关
    52. //所以每个连接结束的时候都需要进行关闭
    53. clientSocket.close();
    54. }
    55. }
    56. private String process(String request) {
    57. return request;
    58. }
    59. public static void main(String[] args) throws IOException {
    60. TcpEchoServer server=new TcpEchoServer(9090);
    61. server.start();
    62. }
    63. }

    客户端

    1. public class TcpEchoServer {
    2. private ServerSocket listenSocket=null;
    3. public TcpEchoServer(int port) throws IOException {
    4. listenSocket=new ServerSocket(port);
    5. }
    6. public void start() throws IOException {
    7. System.out.println("程序启动");
    8. while(true){
    9. //UDP的服务器进入主循环,就直接尝试receive请求
    10. //但是TCP是有连接的,首先需要做的是建立好连接
    11. //当服务器运行的时候,如果客户端没有建立连接,accept就会阻塞等待
    12. //如果客户端建立连接了,accept就会返回一个socket对象
    13. //进一步的服务器和客户端之间的交互,就交给clientSocket来完成了
    14. Socket clientSocket=listenSocket.accept();
    15. processConnection(clientSocket);
    16. }
    17. }
    18. private void processConnection(Socket clientSocket) throws IOException {
    19. String log=String.format("[%s:%d] 客户端上线!",
    20. clientSocket.getInetAddress().toString(),
    21. clientSocket.getPort());
    22. System.out.println(log);
    23. //TCP socket也是字节流,进行数据读写时跟之前读写文件是类似的,也是使用InputStream和OutputStream
    24. try(InputStream inputStream=clientSocket.getInputStream();
    25. OutputStream outputStream= clientSocket.getOutputStream()){
    26. while(true){
    27. //1、读取请求并解析
    28. Scanner sc=new Scanner(inputStream);
    29. if(!sc.hasNext()) {
    30. log = String.format("[%s:%d] 客户端下线!",
    31. clientSocket.getInetAddress().toString(),
    32. clientSocket.getPort());
    33. System.out.println(log);
    34. break;
    35. }
    36. String request=sc.next();
    37. //2、根据请求计算响应
    38. String response=process(request);
    39. //3、把响应写回客户端
    40. PrintWriter writer=new PrintWriter(outputStream);
    41. writer.println(response);
    42. writer.flush();
    43. log=String.format("[%s:%d] req: %s; resp: %s",
    44. clientSocket.getInetAddress().toString(),clientSocket.getPort(),
    45. request,response);
    46. System.out.println(log);
    47. }
    48. }catch(IOException e){
    49. e.printStackTrace();
    50. }finally {
    51. //当前clientSocket的生命周期并不是跟随整个程序的,而是和连接有关
    52. //所以每个连接结束的时候都需要进行关闭
    53. clientSocket.close();
    54. }
    55. }
    56. private String process(String request) {
    57. return request;
    58. }
    59. public static void main(String[] args) throws IOException {
    60. TcpEchoServer server=new TcpEchoServer(9090);
    61. server.start();
    62. }
    63. }

    以上就是一个简单的基于TCP Socket 的回显程序,但是还存在问题,在实际开发中,一个服务器可以对应很多个客户端,但是在这个程序中我们可以发现这个服务器只能对应一个客户端,客户端1下线之后,客户端2才上线,不符合逻辑,所以我们需要使用多线程来处理,使得一个服务器可以对应多个客户端。

     多线程处理的程序:

    因为根据程序我们可以知道是服务器这里的逻辑有些问题,当没有客户端建立连接的时候,此时服务器就阻塞在accept方法上,当有客户端连接的时候,accept返回得到clientSocket并且进入processConnection方法,此时在这个方法中,代码就在sc.next()请求这里阻塞,如果有请求,结束阻塞读取数据,处理完请求之后又回到hasNext()这里阻塞,相当于说客户端什么时候退出,循环啥时候结束。此时我们需要使用多线程,让主线程里面循环调用accept,每次获取到一个连接,就创建一个线程,让这个线程来处理这个连接。

    代码示例:

    1. public class TcpThreadEchoServer {
    2. private ServerSocket listenSocket=null;
    3. public TcpThreadEchoServer(int port) throws IOException {
    4. listenSocket=new ServerSocket(port);
    5. }
    6. public void start() throws IOException {
    7. System.out.println("服务器启动");
    8. while(true){
    9. Socket clientSocket=listenSocket.accept();
    10. Thread t=new Thread(){
    11. @Override
    12. public void run() {
    13. try {
    14. processConnection(clientSocket);
    15. } catch (IOException e) {
    16. throw new RuntimeException(e);
    17. }
    18. }
    19. };
    20. t.start();
    21. }
    22. }
    23. private void processConnection(Socket clientSocket) throws IOException {
    24. String log=String.format("[%s:%d] 客户端上线!",
    25. clientSocket.getInetAddress().toString(),
    26. clientSocket.getPort());
    27. System.out.println(log);
    28. //TCP socket也是字节流,进行数据读写时跟之前读写文件是类似的,也是使用InputStream和OutputStream
    29. try(InputStream inputStream=clientSocket.getInputStream();
    30. OutputStream outputStream= clientSocket.getOutputStream()){
    31. while(true){
    32. //1、读取请求并解析
    33. Scanner sc=new Scanner(inputStream);
    34. if(!sc.hasNext()) {
    35. log = String.format("[%s:%d] 客户端下线!",
    36. clientSocket.getInetAddress().toString(),
    37. clientSocket.getPort());
    38. System.out.println(log);
    39. break;
    40. }
    41. String request=sc.next();
    42. //2、根据请求计算响应
    43. String response=process(request);
    44. //3、把响应写回客户端
    45. PrintWriter writer=new PrintWriter(outputStream);
    46. writer.println(response);
    47. writer.flush();
    48. log=String.format("[%s:%d] req: %s; resp: %s",
    49. clientSocket.getInetAddress().toString(),clientSocket.getPort(),
    50. request,response);
    51. System.out.println(log);
    52. }
    53. }catch(IOException e){
    54. e.printStackTrace();
    55. }finally {
    56. clientSocket.close();
    57. }
    58. }
    59. private String process(String request) {
    60. return request;
    61. }
    62. public static void main(String[] args) throws IOException {
    63. TcpThreadEchoServer server=new TcpThreadEchoServer (9090);
    64. server.start();
    65. }
    66. }

     只有服务器这里让主线程里面循环调用accept,每次获取到一个连接,就创建一个线程,让这个线程来处理这个连接。

     运行结果:

    但同时使用多线程的时候,如果此时服务器频繁创建销毁线程,需要很大的成本,所以此时我们还可以用线程池来改善这个问题,就不需要频繁的创建销毁。

    代码示例:

    1. public class TcpThreadPoolEchoServer {
    2. private ServerSocket listenSocket = null;
    3. public TcpThreadPoolEchoServer(int port) throws IOException {
    4. listenSocket = new ServerSocket(port);
    5. }
    6. public void start() throws IOException {
    7. System.out.println("服务器启动");
    8. ExecutorService executorService = Executors.newCachedThreadPool();
    9. while (true) {
    10. Socket clientSocket = listenSocket.accept();
    11. executorService.submit(new Runnable() {
    12. @Override
    13. public void run() {
    14. try {
    15. processConnection(clientSocket);
    16. } catch (IOException e) {
    17. throw new RuntimeException(e);
    18. }
    19. }
    20. });
    21. }
    22. }
    23. private void processConnection(Socket clientSocket) throws IOException {
    24. String log = String.format("[%s:%d] 客户端上线!",
    25. clientSocket.getInetAddress().toString(),
    26. clientSocket.getPort());
    27. System.out.println(log);
    28. //TCP socket也是字节流,进行数据读写时跟之前读写文件是类似的,也是使用InputStream和OutputStream
    29. try (InputStream inputStream = clientSocket.getInputStream();
    30. OutputStream outputStream = clientSocket.getOutputStream()) {
    31. while (true) {
    32. //1、读取请求并解析
    33. Scanner sc = new Scanner(inputStream);
    34. if (!sc.hasNext()) {
    35. log = String.format("[%s:%d] 客户端下线!",
    36. clientSocket.getInetAddress().toString(),
    37. clientSocket.getPort());
    38. System.out.println(log);
    39. break;
    40. }
    41. String request = sc.next();
    42. //2、根据请求计算响应
    43. String response = process(request);
    44. //3、把响应写回客户端
    45. PrintWriter writer = new PrintWriter(outputStream);
    46. writer.println(response);
    47. writer.flush();
    48. log = String.format("[%s:%d] req: %s; resp: %s",
    49. clientSocket.getInetAddress().toString(), clientSocket.getPort(),
    50. request, response);
    51. System.out.println(log);
    52. }
    53. } catch (IOException e) {
    54. e.printStackTrace();
    55. } finally {
    56. clientSocket.close();
    57. }
    58. }
    59. private String process(String request) {
    60. return request;
    61. }
    62. public static void main(String[] args) throws IOException {
    63. TcpThreadPoolEchoServer server=new TcpThreadPoolEchoServer(9090);
    64. server.start();
    65. }
    66. }

    例子:基于TCP Socket实现一个翻译程序

    1. //观察之前的程序,我们可以知道,其实 echo 回显服务器和 dict 翻译服务器之间, 主要的区别就是 process 方法.
    2. // 其他的都差不多, 因此就可以通过继承的手段来复用代码.
    3. // 只需要重写 process 方法, 重新实现根据请求计算响应的逻辑即可.
    4. public class TcpDictServer extends TcpThreadPoolEchoServer{
    5. private HashMap dict=new HashMap<>();
    6. public TcpDictServer(int port) throws IOException {
    7. super(port);
    8. dict.put("hello","你好");
    9. dict.put("fruit","水果");
    10. dict.put("animal","动物");
    11. }
    12. @Override
    13. public String process(String request) {
    14. return dict.getOrDefault(request,"[单词不存在]");
    15. }
    16. public static void main(String[] args) throws IOException {
    17. TcpDictServer server=new TcpDictServer(9090);
    18. server.start();
    19. }
    20. }

    🥬小结

    以上就是今天的内容,有什么问题大家都可以在评论区留言🍉🍉🍉

     

  • 相关阅读:
    【JavaSE】文件与IO流
    PyCharm 的初始设置
    C++之this指针总结(二百二十)
    Java RedshiftClient 封装
    【opencv-c++】图像对象的赋值,拷贝和创建
    冯诺依曼体系结构与进程的初步理解
    Nacos Config
    (转载)基于量子遗传算法的函数寻优算法
    码蹄集 - MT2094 - 回文之时:第4组数据错误
    华为机试真题 C++ 实现【最大括号深度】
  • 原文地址:https://blog.csdn.net/m0_65673419/article/details/126216453