Linux socket api

1,151 阅读5分钟
原文链接: www.daemoncoder.com

创建socket

linux在 sys/socket.h 下定义了 socket() 系统调用,来创建一个socket,返回一个文件描述符,读写文件的函数也可以用来操作socket。
#include <sys/socket.h>
int socket(int domain, int type, int protocal);
  • domain参数
在之前 Linux socket地址结构体 文章中介绍了三种协议族,domain参数用来指定用哪种协议族,常见的是 PF_INET(IPv4)、PF_INET6(IPv6) 和 PF_UNIX(Unix本地域)。
  • type参数
type参数指定服务类型,常见的是SOCK_STREAM(流服务)、SOCK_DGRAM(数据报)、SOCK_RAW(原始套接字)。对于TCP/IP协议族,需要指定type为 SOCK_STREAM,如果想使用UDP协议则用 SOCK_DGRAM。SOCK_RAW 除了可以处理普通的网络报文之外,可以用来处理前面所述无法处理的报文,如ICMP、IGMP等,可以由用户构造IP头,总体来说,SOCK_RAW 可以处理一些特殊协议报文以及操作IP层及其以上的数据。
  • protocal参数
protocal参数表示在前面两个参数筛选出的协议集合中,再指定一个具体的协议,传入0表示使用默认协议。通常前两个参数就可以确定一个要使用的协议了,一般这个参数传0就可以。
  • 返回
socket()返回一个文件描述符,对socket操作的其他函数,往往需要传入这个值来指定要操作的是哪个socket。socket()调用失败返回-1,并设置errno。用户可以 #include <errno.h>,然后使用errno变量。

命名socket

通常,网络编程中服务器端程序要绑定一个socket地址,如tcp上的80端口,来提供给客户端连接。给socket绑定地址叫做命名socket,不命名socket表示采用操作系统自动分配的地址。服务器端通常需要命名socket,客户端一般不需要。linux提供的命名socket的系统调用为:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd参数
前面所述socket()返回的文件描述符。
  • addr参数
指定要绑定的socket地址,sockaddr类型的指针,可以由sockaddr_in、sockaddr_in6、sockaddr_un等类型强转过来,详见 Linux socket地址结构体
  • addrlen参数
上述addr参数所指结构的大小。
  • 返回
成功时返回0,失败返回-1并设置errno。bind()常见的errno值为 EACCES 和 EADDRINUSE。EACCES表示当前用户没有权限绑定的地址,如端口0~1023。EADDRINUSE表示当前地址已经被占用。

监听socket

命名socket之后,还不能马上接收连接,需要创建一个队列,以便客户端连接较多时,存储待处理的连接。如果队列被占满,后续再在客户端连接,会收到 ECONNREFUSED 错误,即连接被拒绝。linux提供了listen系统调来创建这个队列:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • sockfd参数
前面所述socket()返回的文件描述符。
  • backlog参数
指定队列的大小。
  • 返回
成功时返回0,失败返回-1并设置errno。

接受连接

linux提供了accept系统调用来接受连接:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd参数
前面所述socket()返回的文件描述符。
  • addr参数
接受客户端连接的socket地址。
  • addrlen参数
上述addr参数所指结构的大小。
  • 返回
失败返回-1并设置errno。成功时,返回一个新的文件描述符,对应接受的客户端连接,后续与客户端通信时读写此文件描述符即可。

读写数据

文件读写的函数同样可以用来读写socket,但是系统还提供了专门用于socket数据读写的函数:
#include <sys/socket.h>
// 用于处理TCP的函数
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 用于处理UDP的函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

发起连接

connect函数用于客户端发起连接。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
成功返回0,失败返回-1并设置errno。常见的errno为 ECONNREFUSED 和 ETIMEDOUT,分别表示服务端拒绝连接、连接超时。

关闭连接

关闭文件的函数可以用来关闭socket连接,如close()函数:
#include <unistd.h>
int close(int sockfd);
当在多进程编程中,一个进程fork之后得到的父子进程都可以操作原来的socket,这种情况下fork时会把原来父进程中打开的socket的引用记数加1,无论是在父进程还是子进程中,调用close()时只是把socket的引用记数减1,减到0时才会真正地关闭连接。如果想要立即关闭连接,而不是减引用记数的话,可以用专门针对网络编程的shutdown()函数:
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
howto参数提供了三种关闭方式:
  • SHUT_RD:关闭sockfd上的读,应用程序不能再执行读操作,并且socket接收缓冲区中的数据都被丢弃。
  • SHUT_WR:关闭写,应用程序不能再执行写操作,但是发送缓冲区中的数据会在真正关闭连接之前全部发送出去。
  • SHUT_RDWR:同时关闭读写。
shutdown()成功时返回0,失败返回-1并设置errno。

服务器端示例

#include <stdio.h>
#include <assert.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

void demo() {
    // www.daemoncoder.com
    const char *ip = "127.0.0.1";
    int port = 10086;

    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(sockfd > 0);

    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
    if (-1 == ret) {
        if (errno == EACCES) {
            exit(1);        // 没有权限
        } else if (errno == EADDRINUSE) {
            exit(2);        // 端口被占用
        } else {
            exit(errno);
        }
    }

    ret = listen(sockfd, 1024);
    assert(0 == ret);

    int i = 0;
    while (i++ < 3) {
        struct sockaddr_in client;
        socklen_t client_addrlen = sizeof(client);
        int connfd = accept(sockfd, (struct sockaddr*)&client, &client_addrlen);
        if (connfd < 0) {
            printf("accept failed, errno:%d\n", errno);
        } else {
            char client_ip[INET_ADDRSTRLEN];
            printf("connected with %s:%d\n", inet_ntop(AF_INET, &client.sin_addr, client_ip, INET_ADDRSTRLEN), ntohs(client.sin_port));
            char data[1024];
            recv(connfd, data, 1024, 0);
            printf("receive data:%s\n", data);
        }
    }

    close(sockfd);
}

客户端端示例

#include <stdio.h>
#include <sys/socket.h>
#include <assert.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>

void demo() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(sockfd > 0);

    char *server_ip = "127.0.0.1";
    int server_port = 10086;
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    inet_pton(AF_INET, server_ip, &server_addr.sin_addr);
    server_addr.sin_port = htons(server_port);

    int ret = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (-1 == ret) {
        printf("connect failed, errno:%d\n", errno);
    }

    char data[] = "www.daemoncoder.com";
    send(sockfd, data, strlen(data), 0);

    close(sockfd);
}