登录后台

页面导航

UNIX/Linux 中的 socket 是什么

在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。

为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:

  • 通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
  • 通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。

UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。

请注意,网络连接也是一个文件,它也有文件描述符。

可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:

  • 用 read() 读取从远程计算机传来的数据;
  • 用 write() 向远程计算机写入数据

socket类型

比较常用的类型包括:流式套接字(使用TCP),数据报套接字(UDP),原始套接字等等。主要会用到:流式套接字SOCK_STREAM和数据报套接字SOCK_DGRAM。

SOCK_STREAM流式套接字

流式套接字是面向连接的套接字,在程序中用SOCK_STREAM 表示。SOCK_STREAM是一种靠得住、双向的通讯流。它有如下几个特点:

  • 数据在传输过程中不会消失
  • 数据是按照顺序传输的
  • 数据的发送和接收不是同步的

流式套接字能有这么高质的数据传输是因为它使用的是TCP协议

面向连接的套接字在正式通信之前要先确定一条路径,没有特殊情况的话,以后就固定使用这条路径来传递数据包了。当然,路径被破坏的话,比如某个路由器断电了,那么会重新建立路径。

为了保证数据包准确、顺序地到达,发送端在发送数据包以后,必须得到接收端的确认才能发送下一个数据包;如果数据包已经发出去了,在数据包发出去的一段时间后仍没有接收端的回应,那么发送端就会重新再发送一次,直到得到接收端的回应。这样就能保证,发送端发送的所有数据包都能准确有序地到达接收端

SOCK_DGRAM数据报套接字

数据报套接字是面向无连接的套接字,在程序中用SOCK_DGRAM 表示。SOCK_DGRAM是靠不住,无序的通讯流。它有如下几个特点:

  • 快速传输但无序
  • 传输的数据可能丢失也可能损坏
  • 限制每次传输的数据大小
  • 数据的发送和接收时同步的

数据报套接字使用的是 UDP 协议。

相关概念

  • 流:计算机中的流其实是一种信息的转换。于某一对象,通常把对象接收外界的信息输入称为输入流,把对象向外输出信息为输出流,合称为输入/输出流(I/O Stream)。对象间进行数据交换时总是先将数据转换为某种形式的流,再通过流的传输,到达目的对象后再将流转换为对象数据。所以,可以把流看作是一种数据的载体,通过它可以实现数据交换和传输。
  • 阻塞block:阻塞调用是指调用结果返回(或者收到通知)之前,当前线程会被挂起,即不继续执行后续操作
  • 非阻塞non-block:非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
  • 同步synchronous:发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法
  • 异步asynchronous:发出一个功能调用后,不管没有结果的返回,都不影响当前任务的继续执行。即两个生产线相互独立
  • 带外数据outband data:带外数据,也称为TCP紧急数据,是相连的每一对流套接口间一个逻辑上独立的传输通道。带外数据是独立于普通数据传送给用户的

TCP/IP协议族

在TCP与IP协议族里有很多协议,比如TCP协议、UDP协议、IP协议、FTP协议等等。计算机有了这些协议,就可以和其他的计算机终端做自由的交流了

TCP/IP协议族四层分层依次是:链路层、网络层、传输层、应用层

  • 应用层(Telnet、FTP、e-mail等):向用户提供一组常用的应用程序,比如电子邮件、文件传输访问、远程登录等。远程登录TELNET使用TELNET协议提供在网络其它主机上注册的接口。TELNET会话提供了基于字符的虚拟终端。文件传输访问FTP使用FTP协议来提供网络内机器间的文件拷贝功能。
  • 传输层(TCP和UDP):提供应用程序间的通信。其功能包括:一、格式化信息流;二、提供可靠传输。为实现后者,传输层协议规定接收端必须发回确认,并且假如分组丢失,必须重新发送。
  • 网络层:负责相邻计算机之间的通信,功能包括三方面
    • 处理来自传输层的分组发送请求,收到请求后,将分组装入IP数据报,填充报头,选择去往信宿机的路径,然后将数据报发往适当的网络接口
    • 处理输入数据报:首先检查其合法性,然后进行寻径–假如该数据报已到达信宿机,则去掉报头,将剩下部分交给适当的传输协议;假如该数据报尚未到达信宿,则转发该数据报
    • 处理路径、流控、拥塞等问题
  • 链路层:是TCP/IP软件的最低层,负责接收IP数据报并通过网络发送之,或者从网络上接收物理帧,抽出IP数据报,交给IP层

TCP

TCP协议英文全拼为Transmission Control Protocol,中文意思是传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。比如说用手机打电话必须等对方接通后才能聊天,所以说TCP连接就只适合双方的通信。

从这个分析可以看出,建立连接可以在需要在双方建立一个传递信息的通道,在发送方发送请求连接信息接收方响应后才能开始传递信息,而且是在一个通道中传送,因此接受方能比较完整地收到发送方发出的信息,即信息传递的可靠性比较高。但也正因为需要建立连接,使资源开销加大(在建立连接前必须等待接受方响应,传输信息过程中必须确认信息是否传到及断开连接时发出相应的信号等),独占一个通道,在断开连接钱不能建立另一个连接。通信TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。

UDP

UDP协议英文全拼为User Datagram Protocol,中文意思是用户数据报协议,是一个简单的面向数据报也就是面向无连接的传输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地,比如用手机发短信,不用等对方应答,由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。但是因为它是先不需要接受方的响应,因而在一定程度上也无法保证信息传递的可靠性了,也就像写信一样,我们只是将信寄出去,却不能保证收信人一定可以收到。

TCP服务端流程

image.png

socket

int socket( int af, int type, int protocol);
// 第一个参数af指明了协议族,通常用AF_INET、AF_INET6、AF_LOCAL、AF_UNIX等
// 第二个参数type是Socket类型,常用的Socket类型分别是SOCK_STREAM和SOCK_DGRAM
// 第三个参数protocol表示传输协议一般取为0。因为一般情况下有了 domain和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型
int server_fd = socket(AF_UNIX, SOCK_STREAM, 0)

成功返回非负值,表示套接字的文件描述符,失败返回-1

bind

将套接字绑定到本地地址和端口上

int bind(int sockfd, const struct sockaddr_in *addr, int addrlen);
// 第一个参数sockfd为套接字的文件描述符
// 第二个参数addr 为 sockaddr 结构体变量的指针。
// 第三个参数addrlen为addr 变量的大小,可由 sizeof() 计算得出

// **********************************************************************
struct sockaddr {
    unsigned short sa_family;         /* address family, AF_xxx */
    char sa_data[14];                 /* 14 bytes of protocol address */
};
// **********************************************************************
struct sockaddr_in {
    short int sin_family;              /* Address family */
    unsigned short int sin_port;       /* Port number */
    struct in_addr sin_addr;           /* Internet address */
    unsigned char sin_zero[8];         /* Same size as struct sockaddr */
};
// **********************************************************************
struct sockaddr_un {
        sa_family_t sun_family;               /* AF_UNIX */
        char        sun_path[108];            /* Pathname */
}; 
// **********************************************************************

struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sunfamily = AF_UNIX;
strcpy(server_addr.sun_path, "xxxxxx", sizeof(server_addr.sun_path) - 1);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr))

成功返回非负值,失败返回-1,最常见的错误一般是端口被占用。需要注意的是,在Linux系统中,1024以下的端口都需要root权限的程序才可以绑定

listen

让socket进入被动监听状态。当没有客户端请求时,socket处于“沉睡”中,只有当接收到客户端请求时,socket才会被“叫醒”来响应请求。

int listen(int sockfd, intqueue_length);
// 第一个参数sockfd为套接字的文件描述符
// 第二个参数queue_length用于指定接收队列的长度,也就是在Server程序调用accept函数之前最大允许进入的连接请求数,多余的连接请求将被拒绝,典型取值为5。
listen(server_fd, 5);

accept

在listen监听到有新客户端时,就可以用accept函数响应客户的连接请求,建立与客户端的连接。产生一个新的socket描述符来描述该连接,这个连接用来与发起该连接请求的客户交换数据。

int accept(int sockfd, struct sockaddr *addr, int *addrlen);
// 第一个参数sockfd为套接字的文件描述符
// 第二个参数addr为 sockaddr 结构体变量的指针,这个参数是指针类型,是向外传内容的,即addr将在函数调用后填入对方(客户端)的地址信息,如对方的IP、端口等
// 第三个参数addrlen为 addr变量的大小,可由 sizeof() 计算得出。

int client_fd = accept(server_fd, NULL, NULL);

recv

接收客户端或服务端传来的数据,也就是客户端和服务端都要用到

int recv(int aID, char *buf, int len, int flags);

第一个参数aID,表示连接成功的套接字描述符。

注意:这一步对于服务端而言是上一步accept的返回值;对于客户端而言是connect的返回值,并非是第一步socket创建套接字的返回值

第二个参数buf,就是为要接收的数据所在的缓冲区地址,也就是一个空的字符数组的首地址,这里放结果。

第三个参数len为要接收数据的字节数。

第四个参数flags为发送数据时的附带标记 ,一般情况下设置为0。但可以选择下列设置:

MSG_DONTROUTE:表示不使用指定路由,对send、sendto有效

MSG_PEEK:对recv, recvfrom有效,表示读出网络数据后不清除已读的数据

MSG_OOB:对发送接收都有效,表示发送或接受加急数据

send

发送服务端或客户端的数据

int send(int aID, const char *buf, int len, int flags);

与recv函数的参数类似

server示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SERVER_PATH "./tmp_server"

void handle_client(int client_fd)
{
    char buffer[1024];
    ssize_t bytes_read;

    while(1)
    {
        bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
        if(bytes_read == 0)
        {
            perror("Client disconnected\n");
            break;
        }
        else if(bytes_read < 0)
        {
            perror("Error reading from client");
            break;
        }
        buffer[bytes_read] = '\0';
        printf("Received from client: %s\n", buffer);

        if(write(client_fd, buffer, bytes_read) < 0)
        {
            perror("Error writing to client");
            break;
        }
    }
    close(client_fd);
}

int main()
{
    int server_fd, client_fd;
    struct sockaddr_un server_addr, client_addr;
    pid_t pid;

    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if(server_fd < 0)
    {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SERVER_PATH, sizeof(server_addr.sun_path) - 1);

    unlink(server_addr.sun_path);

    if(bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("Error binding socket");
        exit(EXIT_FAILURE);
    }

    if(listen(server_fd, 5) < 0)
    {
        perror("Error listening");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on %s\n", SERVER_PATH);

    while(1)
    {
        client_fd = accept(server_fd, NULL, NULL);
        if(client_fd < 0)
        {
            perror("Error accepting connection");
            continue;
        }
        printf("New client connected\n");

        pid = fork();
        if(pid < 0)
        {
            perror("Error forking");
            close(client_fd);
            continue;
        }
        else if(pid == 0)
        {
            close(server_fd);
            handle_client(client_fd);
            exit(EXIT_SUCCESS);
        }
        else
        {
            close(client_fd);
        }
    }

    close(server_fd);

    unlink(server_addr.sun_path);

    return 0;
}

client示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SERVER_PATH "./tmp_server"

int main()
{
    struct sockaddr_un server_addr;
    int client_fd;

    client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if(client_fd < 0)
    {
        perror("Error creating socket");
        exit(1);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SERVER_PATH, sizeof(server_addr.sun_path) - 1);

    if(connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("Error connecting to server");
        exit(1);
    }

    printf("Connected to server at %s\n",server_addr.sun_path);

    const char *message = "Hello from client!";
    if(write(client_fd, message, strlen(message)) < 0)
    {
        perror("Error writing to socket");
        exit(1);
    }

    char buffer[1024];
    ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
    if(bytes_read < 0)
    {
        perror("Error reading from socket");
        exit(1);
    }
    buffer[bytes_read] = '\0';
    printf("Received message from server: %s\n", buffer);

    close(client_fd);

    return 0;
}
博主已关闭本页面的评论功能