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

Posted Sun大阳哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java实现一个hello/hi的简单的网络聊天程序相关的知识,希望对你有一定的参考价值。

使用Java的Socket实现客户端和服务器端之间的连接,实现客户端重复发送数据到服务器端的功能。即,用户可以在控制台不断输入内容,并将内容逐一发送给服务端。并在服务端显示。

socket定义

        网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
        Socket的英文原义是"孔"或"插座"。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。
        简单来说,我们网络上的那些数据可以看成自来水管里的那些水,那现在服务器就是 自来水厂 而客户端就是你自己的家,那架设在外面的那些水管就是光纤,电话线等等传播网络信号的介质,现在可以直接用个破水管捅到自来水厂,然后另一端再直接捅进你家吗??这肯定不行,不然我不想要水的时候,水厂还在往死里灌,家里不就水漫金山了,所以这个时候水厂想把水给你送过去就需要做个阀门然后通过这个阀门控制水的流量啊等等,你自己家也同样装一个阀门来控制,不然水费咋算啊,哈哈哈

程序实现如下:

客户端:

import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;


public class Client {

    public static void main(String[] args) {

        Socket socket = null;//与服务器链接的套接字
        Scanner fromKey = null;//键盘输入流
        Scanner fromServer = null;//获取服务器发送的字节流
        PrintWriter toServer = null;//向服务器发送的字节流

        try {
            //链接服务器的套接字
            socket = new Socket("localhost",8866);

            /*
             * 注意:服务器端与客户端不能都先获取输入流,这样会导致socket阻塞
             * */
            //实例化从服务器读的字节流
            fromServer = new Scanner(socket.getInputStream());
            //实例化向服务器写的字节流
            toServer = new PrintWriter(socket.getOutputStream());
            //实例化键盘输入流
            fromKey = new Scanner(System.in);

            while(fromServer.hasNextLine()){
                //阻塞等待服务器发送消息
                String fromServerData = fromServer.nextLine();
                System.out.println("服务器 :" + fromServerData);
                System.out.print("我(客户端) :");
                //获取输入的数据
                String toServerData = fromKey.nextLine();
                //发送给服务器
                toServer.println(toServerData);
                toServer.flush();
            }

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally {
            try {
                fromServer.close();
                toServer.close();
                socket.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }


    }

}

服务端:

import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class Server {

    public static void main(String[] args) {

        ServerSocket ServerSocket = null;//开放的套接字
        Socket socket = null;//链接客户端的套接字
        Scanner fromKey = null;//键盘输入流
        Scanner fromClient = null;//获取从客户端读的字节流
        PrintWriter toClient = null;//获取向客户端写的字节流

        try {
            ServerSocket = new ServerSocket(8866);
            System.out.println("服务器已启动,等待客户端链接");
            //链接客户端的套接字
            socket = ServerSocket.accept();

            //实例化向客户端写的字节流
            toClient = new PrintWriter(socket.getOutputStream());
            toClient.println("你好啊客户端!!!");
            toClient.flush();
            System.out.println("我(服务器端) : 你好啊客户端!!!");
            /*
             * 注意:服务器端与客户端不能都先获取输入流,这样会导致socket阻塞
             * */
            //实例化从服务器读的字节流
            fromClient = new Scanner(socket.getInputStream());
            //实例化键盘输入流
            fromKey = new Scanner(System.in);

            //阻塞等待客户端发送消息
            while(fromClient.hasNextLine()){
                String fromClientData = fromClient.nextLine();
                System.out.println("客户端 :" + fromClientData);
                System.out.print("我(服务器端) :");
                //获取输入的数据
                String toClientData = fromKey.nextLine();
                //发送给客户端
                toClient.println(toClientData);
                toClient.flush();
            }

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally {
            try {
                fromClient.close();
                toClient.close();
                socket.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }


    }

}

执行结果:

Socket通信过程和API全解析

 udp和TCP socket通信过程基本上是一样的,只是调用api时传入的配置不一样,以TCP client/server模型为例子看一下整个过程。

 

1. socket()

  #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int socket(int domain, int type, int protocol);
    
    - 参数说明
    domain: 设定socket双方通信协议域,是本地/internet ip4 or ip6
       Name                Purpose                          Man page
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       AF_INET             IPv4 Internet protocols          ip(7)
       AF_INET6            IPv6 Internet protocols          ipv6(7)

    type: 设定socket的类型,常用的有
        SOCK_STREAM - 一般对应TCP、sctp
        SOCK_DGRAM - 一般对应UDP
        SOCK_RAW - 
        
    protocol: 设定通信使用的传输层协议
    常用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,可以设置为0,系统自己选定。注意protocol和type不是随意组合的。

  socket() API是在glibc中实现的,该函数又调用到了kernel的sys_socket(),调用链如下。

     

        详细的kernel实现我没有去读,大体上这样理解。调用socket()会在内核空间中分配内存然后保存相关的配置。同时会把这块kernel的内存与文件系统关联,以后便可以通过filehandle来访问修改这块配置或者read/write socket。操作socket就像操作file一样,应了那句unix一切皆file。提示系统的最大filehandle数是有限制的,/proc/sys/fs/file-max设置了最大可用filehandle数。当然这是个linux的配置,可以更改,方法参见Increasing the number of open file descriptors,有人做到过1.6 million connection。

2. bind()

#include <sys/types.h>          /* See NOTES */
   #include <sys/socket.h>
   int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
   
   参数说明
   sockfd:之前socket()获得的file handle
   addr:绑定地址,可能为本机IP地址或本地文件路径
   addrlen:地址长度
   
   功能说明
   bind()设置socket通信的地址,如果为INADDR_ANY则表示server会监听本机上所有的interface,如果为127.0.0.1则表示监听本地的process通信(外面的process也接不进啊)。

3. listen()

 #include <sys/types.h>          /* See NOTES */
   #include <sys/socket.h>
   int listen(int sockfd, int backlog);
   
   参数说明
   sockfd:之前socket()获得的file handle
   backlog:设置server可以同时接收的最大链接数,server端会有个处理connection的queue,listen设置这个queue的长度。
   
   功能说明
   listen()只用于server端,设置接收queue的长度。如果queue满了,server端可以丢弃新到的connection或者回复客户端ECONNREFUSED。
   

4. accept()

 #include <sys/types.h>          /* See NOTES */
   #include <sys/socket.h>
   int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
   
   参数说明:
   addr:对端地址
   addrlen:地址长度
   
   功能说明:
   accept()从queue中拿出第一个pending的connection,新建一个socket并返回。
   新建的socket我们叫connected socket,区别于前面的listening socket。
   connected socket用来server跟client的后续数据交互,listening socket继续waiting for new connection。
   当queue里没有connection时,如果socket通过fcntl()设置为 O_NONBLOCK,accept()不会block,否则一般会block。
    

疑问:kernel是如何区分listening socket和connected socket的呢??虽然二者的五元组是不一样的,kernel如何知道通过哪个socket跟APP交互?通过解析内容,是SYN还是数据?暂时存疑。

5. connect()

  #include <sys/types.h>          /* See NOTES */
   #include <sys/socket.h>
   int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
   
   参数说明:
   sockfd: socket的标示filehandle
   addr:server端地址
   addrlen:地址长度
   
   功能说明:
   connect()用于双方连接的建立。
   对于TCP连接,connect()实际发起了TCP三次握手,connect成功返回后TCP连接就建立了。  
   对于UDP,由于UDP是无连接的,connect()可以用来指定要通信的对端地址,后续发数据send()就不需要填地址了。
   当然UDP也可以不使用connect(),socket()建立后,在sendto()中指定对端地址。

  

以上是关于Java实现一个hello/hi的简单的网络聊天程序的主要内容,如果未能解决你的问题,请参考以下文章

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

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

使用java实现一个hello/hi的简单的网络聊天程序

JAVA-hello/hi的简单的网络聊天程序

使用python实现一个hello/hi的简单的网络聊天程序

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