基于Java实现hello/hi简单网络聊天程序

目录

  • Socket简要阐述

    • Socket的概念
    • Socket原理
  • hello/hi的简单网络聊天程序实现
    • 服务器端
    • 客户端
    • 程序执行结果
  • 跟踪分析调用栈 & Linux API对比
    • 创建ServerSocket

      • 调用栈图示
      • 源码分析
    • Socket绑定
      • 调用栈图示
      • 源码分析
    • Socket监听
      • 调用栈图示
      • 源码分析
    • Socket Accept
      • 调用栈图示
      • 源码分析
    • Java Socekt API与Linux Socket API
  • 参考链接

Socket简要阐述

Socket的概念

  • Socket的英文原义是“孔”或“插座”。
    在网络编程中,网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个Socket。
  • Socket套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。
    它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
  • Socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。

    HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

Socket原理

Socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之前,双方必须各自拥有一台电话机一样。

套接字之间的连接过程可以分为三个步骤:服务器监听客户端请求连接确认

  • 服务器监听:建立服务器端套接字,并处于等待连接的状态,不定位具体的客户端套接字,而是实时监控网络状态。
  • 客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。
    为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
  • 连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,
    一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

下图为基于TCP协议Socket的通信模型。

hello/hi的简单网络聊天程序实现

服务器端

实现步骤

1.创建ServerSocket对象,绑定监听端口。
2.通过accept()方法监听客户端请求。
3.连接建立后,在接收进程中通过输入流读取客户端发送的请求信息。
4.在服务器发送进程中通过输出流向客户端发送响应信息。
5.关闭相应的资源和Socket。

package com.socket.MultiThread;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class Server {

    public static ServerSocket serverSocket;
    public static Socket socket;
    public static Scanner scanner;

    /**
     * 构造方法
     * 新建serverSocket和Socket
     */
    public Server() {
        try {
            serverSocket = new ServerSocket(6666);
            System.out.println("Server is working, waiting for client's link");
            socket = serverSocket.accept();
            System.out.println("Client has linked with Server");
        } catch (IOException i) {
            i.printStackTrace();
        }
    }

    /**
     * 服务器端发送消息线程
     * 作用:从键盘读入消息,发送给服务器端
     */
    public class SendThread implements Runnable {
        @Override
        public void run() {
            try {
                OutputStream outputStream = socket.getOutputStream();
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
                PrintWriter printWriter = new PrintWriter(outputStreamWriter, true);

                scanner = new Scanner(System.in);

                while (true) {
                    printWriter.println(scanner.nextLine());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 服务器端接收线程
     * 作用:使用字符流读取缓冲区中客户端所发送的消息
     */
    public class ReceiveThread implements Runnable {
        @Override
        public void run() {
            try {
                InputStream inputStream = socket.getInputStream();
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

                // 输出从客户端端接收到的消息
                while (true) {
                    System.out.println("Client> " + bufferedReader.readLine());
                }
            } catch (IOException i) {
                i.printStackTrace();
            }
        }
    }

    public void start() {
        Thread send = new Thread(new SendThread()); // 发送进程负责服务器端的消息发送
        Thread receive = new Thread(new ReceiveThread()); // 接收进程负责接收客户端的消息
        send.start();
        receive.start();
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

}

客户端

实现步骤

1.创建Socket对象,指明需要连接的服务器的地址和端口号。
2.连接建立后,通过输出流向服务器发送请求信息。
3.通过输入流获取服务器响应的信息。
4.关闭相应资源。

package com.socket.MultiThread;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class Client {

    public static Socket socket;
    public static Scanner scanner;

    /**
     * 构造方法
     * 新建一个socket,并指定了host和port属性,其port与服务器端保持一致
     */
    public Client() {
        try {
            socket = new Socket("127.0.0.1", 6666);
        } catch (IOException i) {
            i.printStackTrace();
        }
    }

    /**
     * 客户端发送消息线程
     * 作用:从键盘读入消息,发送给服务器端
     */
    public class SendThread implements Runnable {
        @Override
        public void run() {
            try {
                OutputStream outputStream = socket.getOutputStream();
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
                PrintWriter printWriter = new PrintWriter(outputStreamWriter, true);

                scanner = new Scanner(System.in);

                while (true) {
                    printWriter.println(scanner.nextLine());
                }
            } catch (IOException i) {
                i.printStackTrace();
            }
        }
    }

    /**
     * 客户端接收线程
     * 作用:使用字符流读取缓冲区中服务器端所发送的消息
     */
    public class ReceiveThread implements Runnable {
        @Override
        public void run() {
            try {
                InputStream inputStream = socket.getInputStream();
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"UTF-8");
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

                // 输出从服务器端接收到的消息
                while (true) {
                    System.out.println("Server> " + bufferedReader.readLine());
                }
            } catch (IOException i) {
                i.printStackTrace();
            }
        }
    }

    public void start() {
        Thread send = new Thread(new SendThread()); // 发送进程负责客户端的消息发送
        Thread receive = new Thread(new ReceiveThread()); // 接收进程负责接收服务器端的消息
        send.start();
        receive.start();
    }

    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }

}

程序执行结果

先运行服务器端,后运行客户端,服务器端在监听客户端的连接请求后建立连接。

服务器端与客户端的交互


跟踪分析调用栈 & Linux API对比

创建ServerSocket

在前面的服务器端代码中,我们创建一个ServerSocket是这样做的:
serverSocket = new ServerSocket(6666);

这行代码在平常看来就是创建了一个端口号为6666的ServerSocket。
但是实际上,我们只是调用了大牛们早已经写好并封装在JDK中的方法,这才能够如此简单地完成套接字的创建。

因此下面通过查看JDK源码,追踪其调用栈来看看ServerSocket的创建究竟是如何实现的。

调用栈图示


通过上图中对于jdk代码中socket创建过程的展示,我们了解到:
在java中ServerSocket的创建主要是调用PlainSocketImpl.socketCreate这个native方法来实现的。

源码分析

那么,我们来康一下这个方法:

/**
* The file descriptor object for this socket.
*/
    protected FileDescriptor fd; // 文件描述符

@Override
    void socketCreate(boolean stream) throws IOException {
        if (fd == null) // 空则抛出异常
            throw new SocketException("Socket closed");

        int newfd = socket0(stream); // 调用jvm的socket0方法来创建新的fd

        fdAccess.set(fd, newfd);
    }

可以看到PlainSocketImpl.socketCreate方法中有一个重要的变量是fd,在代码块中我也将这个变量的声明一并列出了。
记得在本科的Linux课上老师曾经也着重强调了文件描述符这个概念,那么此fd是彼fd吗?
在了解了Linux内核中Socket的建立之后,就能够得出答案:是的。

在jvm中,调用linux底层api: socket()函数时,执行的步骤为:

创建socket结构体
创建tcp_sock结构体,刚创建完的tcp_sock的状态为:TCP_CLOSE
创建文件描述符与socket绑定

因此,在PlainSocketImpl.socketCreate方法中所实现的也正是这样的逻辑。

Socket绑定

上述分析中,我们会发现:
PlainSocketImpl.socketCreate中创建socket时,它并没有绑定任何的ip地址与端口,只是实现了与文件描述符的绑定。
这就有点奇怪了,我们在上面的Java代码中创建ServerSocket的时候明明指定了端口号的呀,怎么调用到底层方法它就把端口号丢了呢?

再次分析源码,原来仅仅是new ServerSocket(6666);这一步操作就调用了三次Linux API,其对应关系如下图。

调用栈图示

同样的,我们可以得出:java中ServerSocket的绑定是调用PlainSocketImpl.socketBind这个native方法来实现的。

源码分析

查看以下JDK源码中PlainSocketImpl.socketBind方法的内容。

@Override
    void socketBind(InetAddress address, int port) throws IOException {
        int nativefd = checkAndReturnNativeFD();

        if (address == null) // ip地址为空则抛出异常
            throw new NullPointerException("inet address argument is null.");

        if (preferIPv4Stack && !(address instanceof Inet4Address)) // 限定IP地址为IPv4版本
            throw new SocketException("Protocol family not supported"); 

        // 调用jvm的bind0方法实现绑定
        bind0(nativefd, address, port, useExclusiveBind);
        if (port == 0) { // 没有给出端口号
            localport = localPort0(nativefd);
        } else {
            localport = port;
        }

        this.address = address;
    }

可以看到,在上面的方法中通过调用bind0这个方法来实现实现的端口号以及IP地址的绑定。
并且,源码限制目前所支持的IP地址是IPv4版本的(虽然目前IPv4地址已经分配完毕),相信在后续的JDK更新中这里会修改过来。

Socket监听

从之前的Java调用Linux API图中可以看到,在完成Socket的创建和绑定之后,服务器端进入监听的状态,等待客户端发出连接的请求。

调用栈图示


从上图可以得出:java中ServerSocket的绑定是调用PlainSocketImpl.socketListen这个native方法来实现的。

源码分析

@Override
    void socketListen(int backlog) throws IOException {
        int nativefd = checkAndReturnNativeFD();

        // 调用jvm的listen0方法实现监听
        listen0(nativefd, backlog);
    }

在JDK中监听的实现较为简单,主要是通过调用JVM中listen0来实现的,这里不做过多的展开。

Socket Accept

服务器端一直被动等待着客户端的连接,终于有一个客户端使用与之匹配的IP地址端口号
并在经历了TCP三次握手之后,客户端建立新的连接Socket对象,服务器就与这个客户端建立了TCP连接

调用栈图示


从上图可以得出:java中ServerSocket的绑定是调用PlainSocketImpl.socketAccept这个native方法来实现的。

源码分析

@Override
    void socketAccept(SocketImpl s) throws IOException {
        int nativefd = checkAndReturnNativeFD();

        if (s == null)
            throw new NullPointerException("socket is null");

        int newfd = -1;
        InetSocketAddress[] isaa = new InetSocketAddress[1];
        if (timeout <= 0) { // 设定有超时计时器
            // 没有超时则调用accept0方法建立连接
            newfd = accept0(nativefd, isaa);
        } else {
            // 否则将该客户端挂入阻塞队列中
            configureBlocking(nativefd, false);
            try {
                waitForNewConnection(nativefd, timeout);
                newfd = accept0(nativefd, isaa);
                if (newfd != -1) {
                    configureBlocking(newfd, true);
                }
            } finally {
                configureBlocking(nativefd, true);
            }
        }

        // 更新socketImpl的文件描述符值
        fdAccess.set(s.fd, newfd);

        // 更新socketImpl中的端口号、ip地址以及localport值
        InetSocketAddress isa = isaa[0];
        s.port = isa.getPort();
        s.address = isa.getAddress();
        s.localport = localport;
        if (preferIPv4Stack && !(s.address instanceof Inet4Address))
            throw new SocketException("Protocol family not supported");
    }

Java Socekt API与Linux Socket API

在上面的调用栈分析中,无论是ServerSocket的创建、绑定、监听,还是连接都伴随着对glibc的调用。

那么glibc到底何许人也?这里引用百度词条的内容:

glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。由于 glibc 囊括了几乎所有的 UNIX 通行的标准,可以想见其内容包罗万象。而就像其他的 UNIX 系统一样,其内含的档案群分散于系统的树状目录结构中,像一个支架一般撑起整个操作系统。在 GNU/Linux 系统中,其C函式库发展史点出了GNU/Linux 演进的几个重要里程碑,用 glibc 作为系统的C函式库,是GNU/Linux演进的一个重要里程碑。

就绑定功能而言,在上述的调用栈追踪中我们知道了所调用的是底层由glibc提供的Bind方法,
但实际上,最终调用内核的SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)。

因此,可以得出结论:

java的socket实现是通过调用操作系统的socket api实现的

参考链接

简单hello/hi程序、分析及Java Socket API与Linux Socket API对比
Linux/Unix socket 基础API (一)
图解Java服务端Socket建立原理
glibc

原文地址:https://www.cnblogs.com/RichardTAO/p/12015145.html

时间: 2024-10-05 04:14:00

基于Java实现hello/hi简单网络聊天程序的相关文章

基于JAVA套接字的简单网络聊天程序

网络中进程之间如何通信 本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类: 消息传递(管道.FIFO.消息队列) 同步(互斥量.条件变量.读写锁.文件和写记录锁.信号量) 共享内存(匿名的和具名的) 远程过程调用(Solaris门和Sun RPC) 但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的.其实TCP/IP协议族已经帮我们解决了这个问题,网

一个hello/hi的简单的网络聊天程序

我选择使用python来实现hello/hi的简单网络聊天程序,源代码包括两个部分,客户端代码和服务器端代码,源代码部分如下图所示: 服务器端代码 1 import socket 2 3 HOST = '127.0.0.1' 4 PORT = 8888 5 6 server = socket.socket() 7 server.bind((HOST, PORT)) 8 server.listen(1) 9 10 print(f'the server is listening at {HOST}:

通过基于java实现的网络聊天程序分析java中网络API和Linux Socket API关系

1. 引言 socket网络编程,可以指定不同的通信协议,在这里,我们使用TCP协议实现基于java的C/S模式下“hello/hi”网络聊天程序 2. 目标 1). 通过该网络聊天程序,了解java Socket API接口的基本用法 2). java Socket API简要介绍 3). linux socket API 简单分析 4). tcp协议的连接和终止 5). 探究java socket API 和 linux socket api之间的关系 3. linux socket API

java实现hello/hi的简单的网络聊天程序与ServerSocket调用栈跟踪

java实现hello/hi的简单的网络聊天程序 网络聊天采用TCP协议通过java实现 import java.io.*; import java.net.Socket; public class Client { public static void main(String[] args) throws Exception{ Socket socket = new Socket("192.168.31.68", 6666); BufferedReader reader = new

基于Python完成一个hello/hi的简单的网络聊天程序

一.Socket 套接字简介 套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开.读写和关闭等操作.套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信.网络套接字是IP地址与端口的组合. 传输层实现端到端的通信,因此,每一个传输层连接有两个端点.那么,传输层连接的端点是什么呢?不是主机,不是主机的IP地址,不是应用进程,也不是传输层的协议端口.传输层连接的端点叫做套接字(socket).根据RFC793的定义:端口号拼接到IP

基于Java NIO的多人在线聊天工具源码实现(登录,单聊,群聊)

近来在学习Java NIO网络开发知识,写了一个基于Java NIO的多人在线聊天工具练练手.源码公开在Coding上: https://coding.net/u/hust_wsh/p/MyChat/git ,开发环境是Ubuntu14.04+Eclipse Mars+JDK1.8. 要想编写一个基于Java NIO的多人在线聊天工具,我总结需要以下几方面的地址:客户端服务器模型,Java NIO中的Selector,SocketChannel,ByteBuffer,Collections以及序

ubuntu系统下使用python3实现简单的网络聊天程序

这是我的第二篇博客,很遗憾第一篇博客没有得到应有的认可. 可能是因为原理介绍和实操部分不够多,只是单纯分析了某一条指令在打开网页过程中,输出的变化. 在我的第二篇博客中把相关原理介绍的更加详细了,同时丰富了程序代码部分的介绍. 本文对通信相关知识点(如socket套接字.TCP/IP.HTTP通信协议).hello/hi网络聊天程序代码.python socke接口与Linux socket api之间的关系三个方面做了相关介绍 一.网络通信相关知识 首先必须明确一点,我们进行网络通信之前,必须

以您熟悉的编程语言为例完成一个hello/hi的简单的网络聊天程序

在这片博文我们将使用python完成一个hello/hi的简单的网络聊天程序 先做一下准备工作 1.linux的socket基础api: 使用socket()创建套接字 int socket(int af, int type, int protocol); af为IP地址类型,AF_INE和AF_INET6分别对应ipv4和ipv6地址type是数据传输方式,Sock_stream(面向连接套接字)和sock_dgram(无连接套接字)protocol是传输协议,IPPROTO_TCP和IPPR

一个hello/hi的简单的网络聊天程序和python Socket API与Linux Socket API之间的关系

1.Socket概述 套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开.读写和关闭等操作.套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信.网络套接字是IP地址与端口的组合. 套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的一个端点.通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机的Socket中,该Socket通过网络接口卡的传输介质将这段信息发送给另一台主机的Socket中,使这段信息能传送到