Linux 网络编程-Socket

套接字

套接字(socket)是一种通信机制,就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。它可以明确地将客户端和服务器区分开来,可以实现一个或多个客户端到服务器的连接。

创建套接字

socket 系统调用创建一个套接字,并返回一个可以用来访问该套接字的描述符。

socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint.

The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process.

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

套接字属性

domain:

通信域,又被称为协议族,指定套接字通信中使用的通信介质

The domain argument specifies a communication domain; this selects the protocol family which will be used for communication.

如下是一些可以指定的协议族,定义在<sys/socket.h>头文件中

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)
AF_IPX              IPX - Novell protocols
AF_NETLINK          Kernel user interface device     netlink(7)
AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
AF_AX25             Amateur radio AX.25 protocol
AF_ATMPVC           Access to raw ATM PVCs
AF_APPLETALK        AppleTalk                        ddp(7)
AF_PACKET           Low level packet interface       packet(7)
AF_ALG              Interface to kernel crypto API

最常用的套接字域有:AF_UNIXAF_INET

  • AF_UNIX 用于通过 UNIX 和 Linux 文件系统实现的本地套接字,该域的底层协议就是文件输入/输出, AF_UNIX 域的套接字提供了一个可靠的双向通信路径。
  • AF_INET 用于 UNIX 网络套接字,AF_INET 套接字可以用于通过包括因特网在内的 TCP/IP 网络进行通信的程序。

type

指定套接字类型

The socket has the indicated type, which specifies the communication semantics.

常用的有 SOCK_STREAMSOCK_DGRAM

SOCK_STREAM 提供有序的、可靠的、面向连接的双向字节流服务。对于 AF_INET 域套接字来说,它默认是通过一个 TCP 连接来提供这一特性的。

SOCK_DGRAM 支持数据报(固定最大长度的无连接、不可靠消息)。对于 AF_INET 域套接字,这种类型的通信是由 UDP 数据报来提供的。

从 Linux 2.6.27 开始,type 参数有第二个用途,除了指定套接字类型之外,它还可以包括以下任何值的按位或,以修改 socket() 的行为。

SOCK_NONBLOCK 在新打开的文件描述上设置 O_NONBLOCK 文件状态标志

SOCK_CLOEXEC 在新文件描述符上设置 close-on-exec (FD_CLOEXEC) 标志

protocol:

通信所用的协议一般由套接字类型和套接字域来决定,通常不需要选择,将该参数设置为 0 ,表示使用默认协议。

IPv4套接字创建

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */

tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
raw_socket = socket(AF_INET, SOCK_RAW, protocol);

SOCK_STREAM 用于打开 tcp 套接字,SOCK_DGRAM 用于打开 udp 套接字,SOCK_RAW 用于打开 raw 套接字,以直接访问 IP 协议。

protocol 是要接收或发送的 IP 0中的 IP 协议。

对于 tcp 套接字来说,protocol 唯一有效值是 0 和 IPPROTO_TCP

对于 udp 套接字来说,protocol 唯一有效值是 0 和 IPPROTO_UDP

对于 SOCK_RAW,可以指定在 RFC 1700 分配的编号中定义的有效 IANA IP 协议。

套接字地址

每个套接字域都有其自己的地址格式。

AF_UNIX 域套接字地址由结构 sockaddr_un 来描述。

#define UNIX_PATH_MAX    108

struct sockaddr_un {
    sa_family_t sun_family;               /* AF_UNIX */
    char        sun_path[UNIX_PATH_MAX];  /* pathname */
};

AF_INET 域中,套接字地址由结构 sockaddr_in 来指定,套接字由它的IP地址端口号来完全确定。

struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};

IP 地址结构 in_addr

/* Internet address. */
    struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};

为套接字分配名称

要想让通过 socket 调用创建的套接字可以被其它进程使用,服务器程序就必须给该套接字命名,这样 AF_UNIX 套接字就会关联到一个文件系统的路径名。 AF_INET 套接字就会关联到一个 IP 和端口号。

命名由 bind 系统调用实现,bind 系统调用把参数 addr 中的地址分配给与 sockfd 关联的未命名套接字,地址长度取决于套接字地址,由 addrlen 传递,由于不同域的套接字地址结构不一致,因此 bind 需要将一个特定的地址结构指针转换为指向通用地址结构类型 (struct sockaddr *) 。

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);

sockaddr 结构的定义就像是:

struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
}

用于转换在 addr 中传递的结构指针,以避免编译警告。

创建套接字队列

为了能在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求,通过系统调用 listen 来完成。

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.

sockfd 参数是一个引用套接字类型为 SOCK_STREAMSOCK_SEQPACKET 的文件描述符。

listen 函数将队列长度设置为 backlog 参数的值,在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字,超过的连接将被拒绝,导致客户连接请求失败。

TCP 套接字上 backlog 参数的行为在 Linux 2.2 中发生了变化。现在它指定等待接受的完全建立的套接字的队列长度,而不是不完整的连接请求的数量。

接受连接

accept 系统调用与基于连接的套接字类型(SOCK_STREAMSOCK_SEQPACKET)一起使用。

accept 系统调用只有当有客户程序试图连接到 sockfd 参数指定的套接字上时才返回,这里客户是指 在套接字队列中排在第一个的未处理连接,它提取侦听套接字的挂起连接队列上的第一个连接请求。

accept 函数将创建一个新的套接字来与客户进行通信,并且返回新的套接字描述符,新创建的套接字不处于监听状态。 原来的套接字 sockfd 不受此调用的影响。sockfd 指定的套接字必须事先由 bind 调用命名,并且由 listen 调用给它分配一个连接队列。连接客户的地址将被放入 addr 所指向的结构中,如果不关心客户的地址,可以指定为空指针 NULL,同时 addrlen 也应该为 NULL

如果队列中没有挂起的连接,并且套接字没有被标记为非阻塞,accept() 会阻塞调用者,直到连接出现。如果套接字被标记为非阻塞并且队列中没有挂起的连接,accept() 将失败并返回 EAGAINEWOULDBLOCK 错误。

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

请求连接

客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器,通过 connect 系统调用来完成,参数 sockfd 指定的套接字将连接到参数 addr 指定的服务器套接字,sockfd 必须是通过 socket 系统调用获得的一个有效的描述符。

如果连接不能立刻建立,connect 调用将阻塞一段不确定的超时时间。一旦这个超时时间到达,连接将被放弃, connect 调用失败。如果 connect 调用 被一个信号中断,而该信号又得到了处理,connect 调用还是会失败(errno被设置为EINTR),但 连接尝试并不会被放弃,而是以异步方式继续建立,程序必须在此后进行检查以查看连接是否成功建立。

connect 调用的阻塞特性可以通过设置文件描述符的 O_NONBLOCK 标志来改变,此时,如果连接不能立刻建立,connect 将失败并把 errno 设置为 EINPROGRESS,而 连接将以异步的方式继续进行

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);

关闭套接字

通过 close 函数来终止客户和服务器上的套接字连接,就如同对底层文件描述符进行关闭一样。

#include <unistd.h>

int close(int fd);

TCP Socket 编程流程

新创建的 TCP 套接字没有远程或本地地址,也没有完全指定。要创建传出 TCP 连接,请使用 connect 建立到另一个 TCP 套接字的连接。要接收新的传入连接,首先将套接字 bind 到本地地址和端口,然后调用 listen 使套接字进入侦听状态。之后,可以使用 accept 接受每个传入连接的新套接字。已成功调用 accept 或 connect 的套接字已完全指定并可传输数据。无法在侦听或尚未连接的套接字上传输数据。

graph TD a(client socket)-->c[connet] c-->d[write/send/sendto] c-.->C d-->e[read/recv/recvfrom] d-->E e-->f(close) A(server socket)-->B[bind] B-->C[listen] C-->D[accept] D-->E[read/recv/recvfrom] E-->F[write/send/sendto] F-->G(close) F-->e

TCP Socket 编程示例

tcp_srv.c

/*************************************************************************
	> File Name: tcp_srv.c
	> Author: shelmean
	> Mail: 
	> Created Time: 2022年 04月 05日 星期二 15:04:03 CST
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <errno.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>


#define LOCAL_HOST_ADDR "127.0.0.1"
#define QUIT "quit"

#define TCP_PORT      (9527)
#define BUFFER_LEN    (1024)

char buf[BUFFER_LEN] = {0};

/**
 * @Name      - 初始化服务端套接字
 * 
 * @Return    - 成功返回服务端套接字描述符,失败返回 -1
 */
int init_server()
{
    int sockfd = -1;
    struct sockaddr_in addr;

    bzero(&addr, sizeof(struct sockaddr_in));

    /* Creating a socket */
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket create failed!");
        return -1;
    }

    /* Naming a socket */
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(LOCAL_HOST_ADDR);
    addr.sin_port = htons(TCP_PORT);

    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0) {
        perror("bind error!");
        return -1;
    }

    /* Creating a Socket Queue */
    if (listen(sockfd, 5) < 0) {
        perror("listen error!");
        return -1;
    }

    return sockfd;
}

int main(int argc, char *argv[])
{
    int srv_sockfd = -1;
    int confd      = -1;
    int ret        = 0;
    socklen_t cli_addr_len;
    struct sockaddr_in cli_addr;

    if ((srv_sockfd = init_server()) < 0) {
        exit(1);
    }

    bzero(&cli_addr, sizeof(struct sockaddr_in));

    /* Accepting Connections */
    cli_addr_len = sizeof(struct sockaddr_in);
    if ((confd = accept(srv_sockfd, (struct sockaddr *)&cli_addr, &cli_addr_len)) < 0) {
        perror("accept error.");
        exit(1);
    }

    /* recv */
    while(1) {
        if ((ret = read(confd, buf, sizeof(buf))) < 0) {
            perror("read failed.");
            exit(1);
        } else if (ret == 0) {
            printf("client closed.\n");
            break;
        }
        if (!strncmp(buf, QUIT, strlen(QUIT))) {
            printf("recv quilt cmd!\n");
            break;
        }
        printf("recv bytes[%d]:%s", ret, buf);
    }

    close(srv_sockfd);

    return 0;
}

tcp_cli.c

/*************************************************************************
	> File Name: tcp_cli.c
	> Author: shelmean
	> Mail: 
	> Created Time: 2022年 04月 05日 星期二 15:04:03 CST
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <errno.h>

#define LOCAL_HOST_ADDR "127.0.0.1"
#define QUIT "quit"

#define TCP_PORT      (9527)
#define BUFFER_LEN    (1024)

char buf[BUFFER_LEN] = {0};


int main(int argc, char *argv[])
{
    int sockfd = -1;
    struct sockaddr_in addr;

    bzero(&addr, sizeof(struct sockaddr_in));

    /* Creating a socket */
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket create failed!");
        exit(1);
    }

    addr.sin_family = AF_INET; //IPv4
    addr.sin_addr.s_addr = inet_addr(LOCAL_HOST_ADDR);
    addr.sin_port = htons(TCP_PORT);

    /* Requesting Connections */
    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0) {
        perror("connect failed!");
        exit(1);
    }

    /* send to server */
    while(1) {
        fgets(buf, sizeof(buf), stdin);
        write(sockfd, buf, strlen(buf) + 1); /* +1 send '\0' */
        if (!strncmp(buf, QUIT, strlen(QUIT))) {
            break;
        }
        memset(buf, 0, sizeof(buf));
    }

    close(sockfd);

    return 0;
}

运行结果:

client 发送数据,服务器收到数据并打印出收到的数据。

参考


百度百科 socket

《Linux man page》

《Linux 程序设计第四版》

posted @ 2022-04-05 15:44  shelmean  阅读(213)  评论(0编辑  收藏  举报