Java Socket编程以及与Linux Socket API关系

Socket 编程(基于Linux)

Socket独立于具体协议的网络编程接口,在ISO模型中,主要位于会话层和传输层之间;在通用的计算机网络五层模型中,主要位于应用层和传输层之间。

Linux Socket

  • 基本上就是BSD Socket
  • 需要使用的头文件
    • 数据类型:#include <sys/types.h>
    • 函数定义:#include <sys/socket.h>

Socket类型

套接字是一种通信机制,通信两方的一种约定,用套接字中的相关函数来完成通信过程。根据传输内容分为流式套接字、数据报套接字、原始套接字;根据使用方式分为主动套接字和被动套接字。

流式套接字(SOCK_STREAM)

提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。(面向TCP)

数据报套接字(SOCK_DGRAM)

提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。(面向UDP)

原始套接字(SOCK_RAW)

可以对较低层次协议,如IP、ICMP直接访问。

主动套接字和被动套接字

创建方式相同,使用方式不同

  • 等待传入连接的套接字——被动,如服务器套接字
  • 发起连接的套接字——主动,如客户套接字

指明端点地址:创建时不指定,使用时指明:

  • TCP/IP需要指明协议端口号和IP地址
  • TCP/IP协议族:PF_INET
  • TCP/IP的地址族:AF_INET

Linux Socket API函数

  1. socket 创建套接字
  2. connect 建立连接
  3. bind 绑定本机端口
  4. listen 监听端口
  5. accept 接受连接
  6. recv, recvfrom 数据接收
  7. send, sendto 数据发送
  8. close, shutdown 关闭套接字

TCP下通信调用Linux Socket API流程如下:


UDP下通信调用Linux Socket API流程如下:

Linux Socket API函数详解

socket函数

int socket( int domain, int type, int protocol)

功能:创建一个新的套接字,返回套接字描述符
参数说明:

  • domain:域类型,指明使用的协议栈,如TCP/IP使用的是 PF_INET
  • type: 指明需要的服务类型, 如:
    • SOCK_DGRAM: 数据报服务,UDP协议
    • SOCK_STREAM: 流服务,TCP协议
  • protocol:一般都取0

举例:s=socket(PF_INET,SOCK_STREAM,0)

connect函数

int connect(int sockfd,struct sockaddr *server_addr,int sockaddr_len)

功能: 同远程服务器建立主动连接,成功时返回0,若连接失败返回-1。

参数说明:

  • Sockfd:套接字描述符,指明创建连接的套接字
  • Server_addr:指明远程端点:IP地址和端口号
  • sockaddr_len :地址长度

bind函数

int bind(int sockfd,struct sockaddr * my_addr,int addrlen)

功能:为套接字指明一个本地端点地址TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟知的端口号,然后等待连接

参数说明:

  • Sockfd:套接字描述符,指明创建连接的套接字
  • my_addr:本地地址,IP地址和端口号
  • addrlen :地址长度

举例:bind(sockfd, (struct sockaddr *)&address, sizeof(address));

listen函数(TCP)

int listen(int sockfd,int input_queue_size)

功能:面向连接的服务器使用它将一个套接字置为被动模式,并准备接收传入连接。用于服务器,指明某个套接字连接是被动的

参数说明:

  • Sockfd:套接字描述符,指明创建连接的套接字
  • input_queue_size:该套接字使用的队列长度,指定在请求队列中允许的最大请求数

举例:listen(sockfd,20)

accept函数(TCP)

int accept(int sockfd, void *addr, int *addrlen);

功能:获取传入连接请求,返回新的连接的套接字描述符。为每个新的连接请求创建了一个新的套接字,服务器只对新的连接使用该套接字,原来的监听套接字接受其他的连接请求。新的连接上传输数据使用新的套接字,使用完毕,服务器将关闭这个套接字。

参数说明:

  • sockfd:套接字描述符,指明正在监听的套接字
  • addr:提出连接请求的主机地址
  • addrlen:地址长度

举例:new_sockfd = accept(sockfd, (struct sockaddr *)&address, &addrlen);

UDP下接收与发送

sendto函数

int sendto(int sockfd, const void * data, int data_len, unsigned int flags, struct sockaddr *remaddr,int remaddr_len)

功能:基于UDP发送数据报,返回实际发送的数据长度,出错时返回-1

参数说明:

  • sockfd:套接字描述符
  • data:指向要发送数据的指针
  • data_len:数据长度
  • flags:一直为0
  • remaddr:远端地址:IP地址和端口号
  • remaddr_len :地址长度

举例:sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&address, sizeof(address));

recvfrom函数

int recvfrom(int sockfd, void *buf, int buf_len,unsigned int flags,struct sockaddr *from,int *fromlen);

功能:从UDP接收数据,返回实际接收的字节数,失败时返回-1
参数说明:

  • sockfd:套接字描述符
  • buf:指向内存块的指针
  • buf_len:内存块大小,以字节为单位
  • flags:一般为0
  • from:远端的地址,IP地址和端口号
  • fromlen:远端地址长度

举例:recvfrom(sockfd,buf,8192,0, ,(struct sockaddr *)&address, &fromlen);

TCP下接收与发送

send函数

int send(int sockfd, const void * data, int data_len, unsigned int flags)

功能:在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1。send会将外发数据复制到OS内核中

参数说明:

  • sockfd:套接字描述符
  • data:指向要发送数据的指针
  • data_len:数据长度
  • flags:一直为0

举例:send(s,req,strlen(req),0);

recv函数

int recv(int sockfd, void *buf, int buf_len,unsigned int flags); 

功能:从TCP接收数据,返回实际接收的数据长度,出错时返回-1。服务器使用其接收客户请求,客户使用它接受服务器的应答。如果没有数据,将阻塞,如果收到的数据大于缓存的大小,多余的数据将丢弃。

参数说明:

  • sockfd:套接字描述符
  • Buf:指向内存块的指针
  • Buf_len:内存块大小,以字节为单位
  • flags:一般为0

举例:recv(sockfd,buf,8192,0)

close函数

close(int sockfd); 

功能:撤销套接字。如果只有一个进程使用,立即终止连接并撤销该套接字,如果多个进程共享该套接字,将引用数减一,如果引用数降到零,则撤销它。

参数说明:

  • Sockfd:套接字描述符

举例:close(socket_descriptor)

转换函数

1.IP地址转换函数

  • inet_addr() 点分十进制数表示的IP地址转换为网络字节序的IP地址
  • inet_ntoa() 网络字节序的IP地址转换为点分十进制数表示的IP地址
    2.字节排序函数
  • htonl 4字节主机字节序转换为网络字节序
  • ntohl  4字节网络字节序转换为主机字节序
  • htons 2字节主机字节序转换为网络字节序
  • ntohs 2字节网络字节序转换为主机字节序

域名解析等相关函数

  • gethostname 获得主机名
  • getpeername 获得与套接口相连的远程协议地址
  • getsockname 获得套接口本地协议地址
  • gethostbyname 根据主机名取得主机信息
  • gethostbyaddr 根据主机地址取得主机信息
  • getprotobyname 根据协议名取得主机协议信息
  • getprotobynumber 根据协议号取得主机协议信息
  • getservbyname 根据服务名取得相关服务信息
  • getservbyport 根据端口号取得相关服务信息
  • getsockopt/setsockopt 获取/设置一个套接口选项
  • ioctlsocket 设置套接口的工作方式

java下的TCP socket编程

java.net.Socket继承于java.lang.Object,有八个构造器,其方法并不多。套接字Socket已经写好封装在java.net.Socket包里。服务器端有特定的ServerSocket方法。

ServerSocket有以下3个属性:

  • SO_TIMEOUT:表示等待客户连接的超时时间。一般不设置,会持续等待。
  • SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。一般不设置,经我的测试没必要,下面会进行详解。
  • SO_RCVBUF:表示接收数据的缓冲区的大小。一般不设置,用系统默认就可以了。

对于Socket类有如下常用方法:

  • accept方法用于产生”阻塞”,直到接受到一个连接,并且返回一个客户端的Socket对象实例。”阻塞”是一个术语,它使程序运行暂时”停留”在这个地方,直到一个会话产生,然后程序继续;通常”阻塞”是由循环产生的。
  • getInputStream方法获得网络连接输入,同时返回一个InputStream对象实例。
  • getOutputStream方法连接的另一端将得到输入,同时返回一个OutputStream对象实例。

注意:其中getInputStream和getOutputStream方法均会产生一个IOException,它必须被捕获,因为它们返回的流对象,通常都会被另一个流对象使用。

java与linux api调用关系

TCP服务器端Socket通信流程

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

TCP客户端通信流程

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

多线程实现服务器与多客户端之间通信步骤

  1. 服务器端创建ServerSocket,循环调用accept()等待客户端连接。
  2. 客户端创建一个socket并请求和服务器端连接。
  3. 服务器端接受客户端请求,创建socket与该客户建立专线连接。
  4. 建立连接的两个socket在一个单独的线程上对话。
  5. 服务器端继续等待新的连接。

多线程下的TCP服务器端:

package socketLearn;


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

public class TcpServerChat {
    public static void main(String[] args) throws IOException {
        System.out.println("--------Server--------");
        ServerSocket server = new ServerSocket(8888);
        //1. 指定端口,使用ServerSocket创建服务器
        while(true){
            //2. 阻塞地等待连接
            Socket socket = server.accept();
            System.out.println("一个客户端建立了连接");
            new Thread(()->{
                DataInputStream dis = null;
                DataOutputStream dos = null;
                BufferedReader br= new BufferedReader(new InputStreamReader(System.in));;
                try {
                    dis = new DataInputStream(socket.getInputStream());
                    dos = new DataOutputStream(socket.getOutputStream());
                } catch (IOException e) {
                    e.printStackTrace();
                }
                boolean isRunning = true;
                while(isRunning){
                    try {
                        //3. 接收消息
                        String msg = dis.readUTF();
                        System.out.println("客户端说:"+msg);

                        //4.返回消息
                        String reMsg = br.readLine();
                        dos.writeUTF(reMsg);
                        dos.flush();

                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    dis.close();
                    socket.close();
                    server.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        }
}

多线程下的TCP客户端:

package socketLearn;

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


public class TcpClientChat {
    public static void main(String[] args) throws IOException {
        System.out.println("--------Client--------");
        //1. 建立连接,使用Socket创建客户端
        Socket client = new Socket("localhost",8888);

        boolean isRunning = true;
        BufferedReader console = new BufferedReader((new InputStreamReader(System.in)));
        DataOutputStream dos = new DataOutputStream(client.getOutputStream());
        DataInputStream dis = new DataInputStream(client.getInputStream());

        while(isRunning){
            //2. 客户端发送消息
            String msg = console.readLine();
            dos.writeUTF(msg);
            dos.flush();
            //3. 获取消息
            msg = dis.readUTF();
            System.out.println("服务器说:"+msg);
        }
        dos.close();
        dis.close();
        client.close();

    }
}

正常来说,客户端打开一个输出流,如果不做约定,也不关闭它,那么服务端永远不知道客户端是否发送完消息,那么服务端会一直等待下去,直到读取超时。

  1. 通过socket关闭
    • 关闭客户端socket,服务器端可执行后续操作,弊端也很明显,客户端不能再次发送消息也不能接收消息,而且要再次发送必须再次创建socket连接
  2. socket关闭输出流

socket.shutdownOutput();

注意不能使用outputStream.close(),若用该法关闭了输出流,那么相应的Socket也将关闭,和直接关闭Socket一个性质。

3. 通过约定的符号
双方约定一个字符或者一个短语,来当做消息发送完成的标识,通常这么做就需要改造读取方法(读取的循环条件)。

4. 通过指定长度
5. 使用DataInputStream、DataOutputStream对socket.getOutputStream,socket.getInputStream进行包装

### UDP Socket编程
* DatagramSocket:用于发送或接收数据包的套接字
* DatagramPacket;数据包
* 不需要利用IO流实现数据的传输每个数据发送单元被统一封装成数据包的方式,发送方将数据包发送到网络中,数据包在网络中去寻找他的目的地。

#### UDP接收方
1、使用DatagramSocket 指定端口创建接收端
2、准备容器封装成DatagramPacket包裹
3、阻塞式接收包裹receive(DatagramPacket p)
4、分析数据byte[] getData();getLength();
5、释放资源

```java
public class UdpServer {
 public static void main(String[] args) throws Exception{
     System.out.println("接收方启动中...");
     DatagramSocket server = new DatagramSocket(9999);

     byte[] container  = new byte[1024*60];
     DatagramPacket packet = new DatagramPacket(container, 0,container.length);
     server.receive(packet);
     byte[] datas = packet.getData();
     int len = packet.getLength();
     System.out.println(new String(datas,0,len,"UTF-8"));

     server.close();
 }
}

UDP发送方

1、使用DatagramSocket 指定端口创建发送端
2、准备数据一定转成字节数组
3、封装成DatagramPacket包裹,需要指定目的地4、发送包裹send(DatagramPacket p)
5、释放资源

public class UdpClient {
    public static void main(String[] args) throws Exception{
        System.out.println("发送方启动中...");
        DatagramSocket client = new DatagramSocket(8888);
        String data = "改革春风吹满地";
        byte[] datas  = data.getBytes();

        DatagramPacket packet = new DatagramPacket(datas, 0,datas.length,
                new InetSocketAddress("localhost",9999));

        client.send(packet);
        client.close();
    }
}
posted @ 2019-12-09 23:12  温冷  阅读(1096)  评论(0编辑  收藏  举报