Java Socket编程基础(1)

参考资料:

  《Java网络编程精解》 孙卫琴

一、socket通信简介

  什么是socket,简单来说,在linux系统上,进程与进程之间的通信称为IPC,在同一台计算机中,进程与进程之间通信可以通过信号、共享内存的方式等等。

  不同计算机上的进程要进行通信的话就需要进行网络通信,而 socket通信就是不同计算机进程间通信中常见的一种方式,当然,同一台计算机也可以通过socket进行通信,比如mysql支持通过unix socket本地连接。

  

  socket在网络系统中拥有以下作用:

    (1) socket屏蔽了不同网络协议之间的差异

    (2) socket是网络编程的入口,它提供了大量的系统调用system call供程序员使用

    (3) linux的重要思想-一切皆文件,socket也是一种特殊的文件,网络通信在linux系统上同样是对文件的读 写操作

  linux上支持多种套接字种类,不同的套接字种类称为"地址簇",这是因为不同的套接字拥有不同的寻址方法。

  linux将其抽象为统一的BSD套接字接口,从而屏蔽了它们的区别,程序员关心了只是BSD套接字接口而已。

  

    以INET套接字为例:

    

  Linux在利用socket()进行系统调用时,需要传递套接字的地址族标识符、套接字类型以及协议、源代码:

  

asmlinkage long sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;
    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        goto out;
    retval = sock_map_fd(sock);
    if (retval < 0)
        goto out_release;
out:
    /* It may be already another descriptor 8) Not kernel problem. */
    return retval;
out_release:
    sock_release(sock);
    return retval;
}

不过对于用户而言,socket就是一种特殊的文件而已....

二、TCP/IP以及SOCKET通信简介

linux上网络通信实现由通信子网和资源子网2部分,

  通信子网位于linux内核空间,由linux内核实现,例如netfilter, tcp/ip协议栈等等功能

  资源子网由位于用户空间的程序实现,例如httpd, nginx, haproxy等等。

计算机通信本质上是进程间的通信,一个计算机上可能运行着多个进程,我们使用端口来标记一个唯一的进程.

  0~1023:管理员才有权限使用,永久地分配给某应用使用;

  注册端口:1024~41951:只有一部分被注册,分配原则上非特别严格;

  动态端口或私有端口:41952+:

  

tcp实现了以下功能:   

①连接建立

②将数据打包成段   MTU通常为1500以下

校验和

③确认、重传以及超时机制

④排序

序列号 32位  并非从0开始  过大的话循环轮换 从0开始

⑤流量控制  速度不同步2台数据的服务器    防止阻塞

缓冲区  发送缓冲    接收缓冲

滑动窗口

⑥拥塞控制  多个进程通信

慢启动   通过慢启动的方式探测,启动的时候很小  随后以指数级增长。

拥塞避免算法

tcp是一个有限状态机,三次连接,四次握手:

注意:如果server端没有调用close()方法,可能出现大量连接处于CLOSE_WAIT状态,占用系统资源。

三、Socket用法

  在C/S通信模式中,客户端主动创建与服务器连接的Socket,服务器收到了客户端的连接请求,也会创建与客户端连接的Socket。

  Socket是通信连接两端的收发器。服务器端监听在某个固定的端口上,每当有一个客户端连入时,都要创建一个socket文件,因此,linux系统打开文件数量直接影响着服务器端socket通信的并发能力。

3.1 构造器

当客户端创建Socket连接Server时,会随机分配端口,因此不用指定

    public static void main(String[] args) throws Exception{
        Socket socket = new Socket();
        //远程服务器地址
        SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
        //设定超时时长,单位ms,为0表示永不超时,超时则跑出SocketTimeoutException
        socket.connect(remoteAddr,60*1000);
    }

设定客户端地址:

  在一个Socket对象中,同时包含了远程服务器的ip地址,端口信息,也要包含客户端的ip地址和端口信息,才能进行双向通信。

  默认,客户端不设置ip的话,客户端地址就是当前客户端主机的地址。构造器中支持显式指定。

Socket的创建和连接中出现的各种异常说明:

(1) UnkownHostException

  无法识别主机名或者ip地址,找不到server主机

(2) ConnectException

  2种情况:

  没有服务器进程监听该端口

  服务器进程拒绝连接:比如服务器端设置了请求队列长度等情形。

(3) SocketTimeoutException

  连接超时

(4) BindException

  无法把Socket对象和指定的本地IP地址或者端口绑定,就会抛出这种异常

  例如:socket.bind(new InetSocketAddress.getByName("222.34.5.7"),1234);

  有可能本地主机没有改地址,或者该端口不能被使用,就会抛出该异常。

3.2 获取Socket信息

Socket包含了连接的相关信息,client和server的地址端口等等,还可以获取InputStream和OutputStream,以下是一个demo

public class HTTPClient {
    String host="www.javathinker.org";
    int port=80;
    Socket socket;

    public void createSocket()throws Exception{
        socket=new Socket("www.javathinker.org",80);
    }

    public void communicate()throws Exception{
        StringBuffer sb=new StringBuffer("GET "+"/index.jsp"+" HTTP/1.1\r\n");
        sb.append("Host: www.javathinker.org\r\n");
        sb.append("Accept: */*\r\n");
        sb.append("Accept-Language: zh-cn\r\n");
        sb.append("Accept-Encoding: gzip, deflate\r\n");
        sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n");
        sb.append("Connection: Keep-Alive\r\n\r\n");

        //发出HTTP请求
        OutputStream socketOut=socket.getOutputStream();
        socketOut.write(sb.toString().getBytes());
        socket.shutdownOutput();  //关闭输出流

        //接收响应结果
        InputStream socketIn=socket.getInputStream();
        ByteArrayOutputStream buffer=new ByteArrayOutputStream();
        byte[] buff=new byte[1024];
        int len=-1;
        while((len=socketIn.read(buff))!=-1){
            buffer.write(buff,0,len);
        }

        System.out.println(new String(buffer.toByteArray()));  //把字节数组转换为字符串

/*
    InputStream socketIn=socket.getInputStream();
    BufferedReader br=new BufferedReader(new InputStreamReader(socketIn));
    String data;
    while((data=br.readLine())!=null){
      System.out.println(data);
    }
*/
        socket.close();
    }

    public static void main(String args[])throws Exception{
        HTTPClient client=new HTTPClient();
        client.createSocket();
        client.communicate();
    }
}

说明:上面方法用ByteArrayOutputStream来接收响应信息,也就是说响应会全部放置在内存中,在响应报文很长的时候这样很不明智,上面注释的代码中演示了如何使用BufferReader逐行进行读取。

3.3 关闭Socket

网络通信占用资源且有太多的因素,在finally代码块中关闭socket是省事的

Socket类提供了3个状态测试方法:

isClosed(): 如果Socket已经连接到远程主机,并且还没有关闭,则返回true

isConnected(): 如果Socket曾经连接到过远程主机,返回true

isBound(): 如果Socket和本地端口绑定,返回true

因此确定一个Socket对象正在处于连接状态,可以用以下方式

boolean isConnected = socket.isConnected() && !socket.isClosed();

3.4 半关闭Socket

socket通信也就是2个进程之间的通信,无论这2个进程是否处于同一个物理机器上,只需要向内核申请注册了端口就可以用ip+port进行唯一的标识。

假设2个进程A和B之间通信,A如何通知B所有数据已经传输完毕呢?

以上文中HttpClient为例

StringBuffer sb=new StringBuffer("GET "+"/index.jsp"+" HTTP/1.1\r\n");
        sb.append("Host: www.javathinker.org\r\n");
        sb.append("Accept: */*\r\n");
        sb.append("Accept-Language: zh-cn\r\n");
        sb.append("Accept-Encoding: gzip, deflate\r\n");
        sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n");
        sb.append("Connection: Keep-Alive\r\n\r\n");

这实际上是典型的HTTP处理的方式,没有请求实体,因此以\r\n\r\n表示结束,这就是一种约定方式。

(1) 如果是字符流,可以以特殊字符作为结束标志,可以是\r\n\r\n,甚至于可以定义为"bye"

(2) A可以先发送一个消息,事先声明了内容长度

(3) A发送完毕之后,主动关闭Socket,B读取完了所有数据也关闭

(4) shutdownInput, shutdownOutput 之关闭输出流或者输出流,但是这并不会释放资源,必须调用Socket的close()方法,才会释放资源

3.5 Socket常用选项

  TCP_NODELAY: 表示立即发送数据,默认是false,表示开启Negale算法,true表示关闭缓冲,确保数据及时发送

    为false时,适合发送方需要发送大批量数据,并且接收方及时响应,这种算法通过减少传输数据的次数来提高效率

    为true,发送方持续的发送小批量数据,并且接受方不一定会立即响应数据

  SO_REUSEADDR: 表示是否允许重用Socket绑定的本地地址

  SO_TIMEOUT: 表示接收数据的等待超时时间

  SO_LINGER: 表示执行Socket的close()方法时,是否立即关闭底层的Socket,哪怕还有数据没有发送完也直接关闭

  SO_SNFBUF: 发送方缓冲区大小

  SO_RCVBUF: 接收数据的缓冲区大小

  SO_KEEPALIVE: 对于长时间处于空闲状态的Socket是否要自动关闭

四、ServerSocket用法

在C/S架构中,服务器端需要创建监听特定端口的ServerSocket,ServerSocket负责接收客户的连接请求。

4.1 ServerSocket

1.必须绑定一个端口

ServerSocket serverSocket = new ServerSocket(80);

  如果无法绑定到一个端口,会抛出BindException,一般由以下原因:

   (1) 端口已经被占用

   (2) 某些操作系统中,只有超级用户才允许使用1-1023的端口

  如果port设置为0,表示操作系统来分配一个任意可用的端口,匿名端口,在某些场合,匿名端口有特殊作用

2. 设定客户连接请求队列的长度

一般的C/S架构中,服务器监听在某个固定的端口上,每来一个客户端连接,服务器都会创建一个socket文件维护与client的通信

管理client连接的任务往往由操作系统来完成。操作系统把这些连接请求存储在一个先进先出的队列中。

许多操作系统限定了队列的最大长度,一般是50。当client connections>50 时,服务器会拒绝新的请求。

对于客户端而言,如果他的请求被server加入了队列,意味着连接成功,这个队列通常称为backlog.

ServerSocket构造方法的backlog参数用来显示指定连接请求队列的长度,它将覆盖操作系统限定的最大长度,不过在以下情形,依旧采用操作系统的默认值:

(1) backlog <= 0

(2) without setting backlog

(3) backlog参数的值 > 操作系统的允许范围

演示: Server端设置backlog为3,不处理请求,client连接超过3会拒绝

import java.io.*;
import java.net.*;
public class Server {
    private int port=8000;
    private ServerSocket serverSocket;

    public Server() throws IOException {
        serverSocket = new ServerSocket(port,3);  //连接请求队列的长度为3
        System.out.println("服务器启动");
    }

    public void service() {
        while (true) {
            Socket socket=null;
            try {
                socket = serverSocket.accept();  //从连接请求队列中取出一个连接
                System.out.println("New connection accepted " +
                        socket.getInetAddress() + ":" +socket.getPort());
            }catch (IOException e) {
                e.printStackTrace();
            }finally {
                try{
                    if(socket!=null)socket.close();
                }catch (IOException e) {e.printStackTrace();}
            }
        }
    }

    public static void main(String args[])throws Exception {
        Server server=new Server();
        Thread.sleep(60000*10);  //睡眠十分钟
        //server.service();
    }
}
import java.net.*;
public class Client {
    public static void main(String args[])throws Exception{
        final int length=100;
        String host="localhost";
        int port=8000;

        Socket[] sockets=new Socket[length];
        for(int i=0;i<length;i++){  //试图建立100次连接
            sockets[i]=new Socket(host, port);
            System.out.println("第"+(i+1)+"次连接成功");
        }
        Thread.sleep(3000);
        for(int i=0;i<length;i++){
            sockets[i].close();  //断开连接
        }
    }
}

3. 设定绑定的IP地址

  一个主机可能有多个地址,此时可以显示指定

        ServerSocket serverSocket = new ServerSocket();
        // 只有在设定地址之前设置才有效
        serverSocket.setReuseAddress(true);
        serverSocket.bind(new InetSocketAddress(8000));    

4. 关闭ServerSocket

  同样应该在finally代码块中调用close()方法,在一般的连接中,往往是由客户端发起请求,也是由客户端发起关闭socket请求。

  但是,在某些keepalive的场景中,例如httpd,nginx等等服务器都支持长连接,通过设定keepalive的最大连接时长和最大连接数来控制长连接。

  此时,那些由于超时的client连接,服务器端会主动发起close()请求。

  如何判断ServerSocket没有关闭

boolean isOpen = serverSocket.isBound() && !serverSocket.isClosed();

4.2 ServerSocket选项

1. SO_TIMEOUT

  accept()方法等待客户端的连接超时时间,以ms为单位,0表示永不超时,默认是0.

  当执行accept()时,如果backlog为空,则服务器一直等待,如果设置了超时时间,则服务器端阻塞在此,超时则抛出SocketTimeoutException

2. SO_REUSEADDR选项

  当服务器因为某些原因需要重启时,如果网络上还有发送到这个ServerSocket的数据,则ServerSocket不会立刻释放该端口,导致重启失败。

  设置为true的话可以确保释放,但是必须在绑定端口之前调用方法。

3. SO_RCVBUF

  接收缓冲大小

五、Demo

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class EchoServer {
  private int port=8000;
  private ServerSocket serverSocket;
  private ExecutorService executorService; //线程池
  private final int POOL_SIZE=4;  //单个CPU时线程池中工作线程的数目

  private int portForShutdown=8001;  //用于监听关闭服务器命令的端口
  private ServerSocket serverSocketForShutdown;
  private boolean isShutdown=false; //服务器是否已经关闭

  private Thread shutdownThread=new Thread(){   //负责关闭服务器的线程
    public void start(){
      this.setDaemon(true);  //设置为守护线程(也称为后台线程)
      super.start();
    }

    public void run(){
      while (!isShutdown) {
        Socket socketForShutdown=null;
        try {
          socketForShutdown= serverSocketForShutdown.accept();
          BufferedReader br = new BufferedReader(
                            new InputStreamReader(socketForShutdown.getInputStream()));
          String command=br.readLine();
         if(command.equals("shutdown")){
            long beginTime=System.currentTimeMillis();
            socketForShutdown.getOutputStream().write("服务器正在关闭\r\n".getBytes());
            isShutdown=true;
            //请求关闭线程池
//线程池不再接收新的任务,但是会继续执行完工作队列中现有的任务
            executorService.shutdown();  

            //等待关闭线程池,每次等待的超时时间为30秒
            while(!executorService.isTerminated())
              executorService.awaitTermination(30,TimeUnit.SECONDS); 

            serverSocket.close(); //关闭与EchoClient客户通信的ServerSocket
            long endTime=System.currentTimeMillis();
            socketForShutdown.getOutputStream().write(("服务器已经关闭,"+
                "关闭服务器用了"+(endTime-beginTime)+"毫秒\r\n").getBytes());
            socketForShutdown.close();
            serverSocketForShutdown.close();

          }else{
            socketForShutdown.getOutputStream().write("错误的命令\r\n".getBytes());
            socketForShutdown.close();
          }
        }catch (Exception e) {
           e.printStackTrace();
        }
      }
    }
  };

  public EchoServer() throws IOException {
    serverSocket = new ServerSocket(port);
    serverSocket.setSoTimeout(60000); //设定等待客户连接的超过时间为60秒
    serverSocketForShutdown = new ServerSocket(portForShutdown);

    //创建线程池
    executorService= Executors.newFixedThreadPool(
        Runtime.getRuntime().availableProcessors() * POOL_SIZE);

    shutdownThread.start(); //启动负责关闭服务器的线程
    System.out.println("服务器启动");
  }

  public void service() {
    while (!isShutdown) {
      Socket socket=null;
      try {
        socket = serverSocket.accept();  //可能会抛出SocketTimeoutException和SocketException
        socket.setSoTimeout(60000);  //把等待客户发送数据的超时时间设为60秒
        executorService.execute(new Handler(socket));  //可能会抛出RejectedExecutionException
      }catch(SocketTimeoutException e){
         //不必处理等待客户连接时出现的超时异常
      }catch(RejectedExecutionException e){
         try{
           if(socket!=null)socket.close();
         }catch(IOException x){}
         return;
      }catch(SocketException e) {
         //如果是由于在执行serverSocket.accept()方法时,
         //ServerSocket被ShutdownThread线程关闭而导致的异常,就退出service()方法
         if(e.getMessage().indexOf("socket closed")!=-1)return;
       }catch(IOException e) {
         e.printStackTrace();
      }
    }
  }

  public static void main(String args[])throws IOException {
    new EchoServer().service();
  }
}
class Handler implements Runnable{
  private Socket socket;
  public Handler(Socket socket){
    this.socket=socket;
  }
  private PrintWriter getWriter(Socket socket)throws IOException{
    OutputStream socketOut = socket.getOutputStream();
    return new PrintWriter(socketOut,true);
  }
  private BufferedReader getReader(Socket socket)throws IOException{
    InputStream socketIn = socket.getInputStream();
    return new BufferedReader(new InputStreamReader(socketIn));
  }
  public String echo(String msg) {
    return "echo:" + msg;
  }
  public void run(){
    try {
      System.out.println("New connection accepted " +
      socket.getInetAddress() + ":" +socket.getPort());
      BufferedReader br =getReader(socket);
      PrintWriter pw = getWriter(socket);

      String msg = null;
      while ((msg = br.readLine()) != null) {
        System.out.println(msg);
        pw.println(echo(msg));
        if (msg.equals("bye"))
          break;
      }
    }catch (IOException e) {
       e.printStackTrace();
    }finally {
       try{
         if(socket!=null)socket.close();
       }catch (IOException e) {e.printStackTrace();}
    }
  }
}
时间: 2024-11-07 14:50:00

Java Socket编程基础(1)的相关文章

Java Socket编程基础篇

原文地址:Java Socket编程----通信是这样炼成的 Java最初是作为网络编程语言出现的,其对网络提供了高度的支持,使得客户端和服务器的沟通变成了现实,而在网络编程中,使用最多的就是Socket.像大家熟悉的QQ.MSN都使用了Socket相关的技术.下面就让我们一起揭开Socket的神秘面纱. Socket编程 网络基础知识点: 两台计算机间进行通讯需要以下三个条件 IP地址.协议.端口号: IP地址:定位应用所在机器的网络位置.(比如家庭住址:北京市朝阳区XX街道XX小区) 端口号

java socket编程基础(转)

一,网络编程中两个主要的问题 一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输. 在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可以唯一地确定Internet上的一台主机. 而TCP层则提供面向应用的可靠(tcp)的或非可靠(UDP)的数据传输机制,这是网络编程的主要对象,一般不需要关心IP层是如何处理数据的. 目前较为流行的网络编程模型是客户机/服务器(C/S)结构.即通信双方一方作为服务器等待客户提出请求并予以响应.客户则

Java Socket编程基础及深入讲解

Socket是Java网络编程的基础,了解还是有好处的, 这篇文章主要讲解Socket的基础编程.Socket用在哪呢,主要用在进程间,网络间通信.本篇比较长,特别做了个目录: 一.Socket通信基本示例 二.消息通信优化 2.1 双向通信,发送消息并接受消息 2.2 使用场景 2.3 如何告知对方已发送完命令 2.3.1 通过Socket关闭 2.3.2 通过Socket关闭输出流的方式 2.3.3 通过约定符号 2.3.4 通过指定长度 三.服务端优化 3.1 服务端并发处理能力 3.2

java socket编程基础

1. [代码]读操作Runable 1 package com.hrd.test.socket; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.PrintWriter; 6 7 /** 8 * 作者: ehomeud 创建于: 2015/4/15 13:15 9 */ 10 public class ReadThread implements Runnable { 11 12 p

Java网络编程基础(六)— 基于TCP的NIO简单聊天系统

在Java网络编程基础(四)中提到了基于Socket的TCP/IP简单聊天系统实现了一个多客户端之间护法消息的简单聊天系统.其服务端采用了多线程来处理多个客户端的消息发送,并转发给目的用户.但是由于它是基于Socket的,因此是阻塞的. 本节我们将通过SocketChannel和ServerSocketChannel来实现同样的功能. 1.客户端输入消息的格式 username:msg    username表示要发送的的用户名,msg为发送内容,以冒号分割 2.实现思路 实现思路与Java网络

【Java】Java Socket编程(1)基本的术语和概念

计算机程序能够相互联网,相互通讯,这使一切都成为可能,这也是当今互联网存在的基础.那么程序是如何通过网络相互通信的呢?这就是我记录这系列的笔记的原因.Java语言从一开始就是为了互联网而设计的,它为实现程序的相互通信提供了许多有用API,这类应用编程接口被称为套接字(Socket).在开始学习Java Socket之前我们需要先来了解一下基本的术语和概念. 1.计算机网络 计算机网络由一组通过通信信道(Communication channel)相互连接的机器组成.这些机器被称为:主机(host

如何为可扩展系统进行Java Socket编程

从简单I/O到异步非阻塞channel的Java Socket模型演变之旅 上世纪九十年代后期,我在一家在线视频游戏工资工作,在哪里我主要的工作就是编写Unix Unix Berkley Socket和Windows WinSock代码.我的任务是确保视频游戏客户端和一个游戏服务器通信.很幸运有这样的机会写一些Java Socket代码,我对Java流式网络编程和简洁明了的API着迷.这一点都不让人惊讶,Java最初就是设计促进智能设备之间的通信,这一点很好的转移到了桌面应用和服务器应用. 19

Java网络编程和NIO详解开篇:Java网络编程基础

Java网络编程和NIO详解开篇:Java网络编程基础 计算机网络编程基础 转自:https://mp.weixin.qq.com/s/XXMz5uAFSsPdg38bth2jAA 我们是幸运的,因为我们拥有网络.网络是一个神奇的东西,它改变了你和我的生活方式,改变了整个世界. 然而,网络的无标度和小世界特性使得它又是复杂的,无所不在,无所不能,以致于我们无法区分甚至无法描述. 对于一个码农而言,了解网络的基础知识可能还是从了解定义开始,认识OSI的七层协议模型,深入Socket内部,进而熟练地

Java Socket编程

对于Java Socket编程而言,有两个概念,一个是ServerSocket,一个是Socket.服务端和客户端之间通过Socket建立连接,之后它们就可以进行通信了.首先ServerSocket将在服务端监听某个端口,当发现客户端有Socket来试图连接它时,它会accept该Socket的连接请求,同时在服务端建立一个对应的Socket与之进行通信.这样就有两个Socket了,客户端和服务端各一个. 客户端写服务端读 服务端代码 public class Server { public s