linux-socket编程socket接口

11次阅读

共计 7008 个字符,预计需要花费 18 分钟才能阅读完成。

常用 socket 函数

Windows 和 Linux 上常用的 socket API 函数并不多,除了特定操作系统提供的一些基于自身系统特性的 API,大多数 Socket API 都源于 BSD Socket(即伯克利套接字(Berkeley Sockets)),因此这些 socket 函数在不同的平台有着相似的签名和参数。

经常有想学习网络编程的新人询问要掌握哪些基础的 socket API,我这里给一个简单的函数列表,列表中给出的都是应该熟练掌握的 socket 函数。

​ 常用 Berkeley Sockets API 一览表

函数名称 函数简单描述 附加说明
socket 创造某种类型的套接字
bind 将一个 socket 绑定一个 ip 与端口的二元组上
listen 将一个 socket 变为侦听状态
connect 试图建立一个 TCP 连接 一般用于客户端
accept 尝试接收一个连接 一般用于服务端
send 通过一个 socket 发送数据
recv 通过一个 socket 收取数据
select 判断一组 socket 上的读事件
gethostbyname 通过域名获取机器地址
close 关闭一个套接字,回收该 socket 对应的资源 Windows 系统中对应的是 closesocket
shutdown 关闭 socket 收或发通道
setsockopt 设置一个套接字选项
getsockopt 获取一个套接字选项

socket

socket()函数的原型如下,这个函数建立一个协议族为 domain、协议类型为 type、协议编号为 protocol 的套接字文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回 -1。

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

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

函数 socket()的参数 domain 用于设置网络通信的域,函数 socket()根据这个参数选择通信协议的族。通信协议族在文件 sys/socket.h 中定义。

名称 含义 名称 含义
PF_UNIX PF_LOCAL 本地通信 PF_X25 ITU-T X25 / ISO-8208 协议
AF_INET,PF_INET IPv4 Internet 协议 PF_AX25 Amateur radio AX.25
PF_INET6 IPv6 Internet 协议 PF_ATMPVC 原始 ATM PVC 访问
PF_IPX IPX-Novell 协议 PF_APPLETALK Appletalk
PF_NETLINK 内核用户界面设备 PF_PACKET 底层包访问
  • type

type 函数 socket()的参数 type 用于设置套接字通信的类型,主要有 SOCKET_STREAM(流式套接字)、SOCK——DGRAM(数据包套接字)等。

名称 含义
SOCK_STREAM Tcp 连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输
SOCK_DGRAM 支持 UDP 连接(无连接状态的消息)
SOCK_SEQPACKET 序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出
SOCK_RAW RAW 类型,提供原始网络协议访问
SOCK_RDM 提供可靠的数据报文,不过可能数据会有乱序

并不是所有的协议族都实现了这些协议类型,例如,AF_INET 协议族就没有实现 SOCK_SEQPACKET 协议类型。

  • protocol

函数 socket()的第 3 个参数 protocol 用于制定某个协议的特定类型,即 type 类型中的某个类型。通常某协议中只有一种特定类型,这样 protocol 参数仅能设置为 0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。

  • errno
    函数 socket()并不总是执行成功,有可能会出现错误,错误的产生有多种原因,可以通过 errno 获得:
含义
EACCES 没有权限建立制定的 domain 的 type 的 socket
EAFNOSUPPORT 不支持所给的地址类型
EINVAL 不支持此协议或者协议不可用
EMFILE 进程文件表溢出
ENFILE 已经达到系统允许打开的文件数量,打开文件过多
ENOBUFS/ENOMEM 内存不足。socket 只有到资源足够或者有进程释放内存
EPROTONOSUPPORT 制定的协议 type 在 domain 中不存在

比如我们建立一个流式套接字可以这样:

int sock = socket(AF_INET, SOCK_STREAM, 0); 

bind

在套接口中,一个套接字只是用户程序与内核交互信息的枢纽,它自身没有太多的信息,也没有网络协议地址和 端口号等信息,在进行网络通信的时候,必须把一个套接字与一个地址相关联,这个过程就是地址绑定的过程。许多时候内核会我们自动绑定一个地址,然而有时用 户可能需要自己来完成这个绑定的过程,以满足实际应用的需要,最典型的情况是一个服务器进程需要绑定一个众所周知的地址或端口以等待客户来连接。这个事由 bind 的函数完成。

int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen)
  • sockfd 就是我们调用 socket 函数后创建的 socket 句柄或者称文件描述符号。
  • addr addr 是指向一个结构为 sockaddr 参数的指针,sockaddr 中包含了地址、端口和 IP 地址的信息。在进行地址绑定的时候,需要弦将地址结构中的 IP 地址、端口、类型等结构 struct sockaddr 中的域进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等接合在一起。

由于历史原因,我们前后有两个地址结构:struct sockaddr 该结构定义如下:

struct sockaddr { 
    uint8_t sa_len;   
    unsigned short sa_family; /* 地址家族, AF_xxx */    
    char sa_data[14]; /*14 字节协议地址 */   
};

其实这个结构逐渐被舍弃,但是也还是因为历史原因,在很多的函数,比如 connect、bind 等还是用这个作为声明,实际上现在用的是第二个结构,我们需要把第二个结构强转成 sockaddr。struct sockaddr_in 其定义如下:

struct sockaddr_in { 
   uint8_t sa_len;   /* 结构体长度 */ 
        short int sin_family; /* 通信类型 */ 
   unsigned short int sin_port; /* 端口 */ 
   struct in_addr sin_addr; /* Internet 地址 */ 
   unsigned char sin_zero[8]; /* 未使用的 */ 
   };

struct in_addr {   //sin_addr 的结构体类型 in_addr 原型
   unsigned long s_addr;     /* 存 4 字节的 IP 地址(使用网络字节顺序)。*/
   };
 

在使用的时候我们必须指定通信类型,也必须把端口号和地址转换成网络序的字节序

  • addrlen addr 结构的长度,可以设置成 sizeof(struct sockaddr)。使用 sizeof(struct sockaddr)来设置套接字的类型和其对已 ing 的结构。

bind()函数的返回值为 0 时表示绑定成功,- 1 表示绑定失败,errno 的错误值如表 1 所示。

含义 备注
EADDRINUSE 给定地址已经使用
EBADF sockfd 不合法
EINVAL sockfd 已经绑定到其他地址
ENOTSOCK sockfd 是一个文件描述符,不是 socket 描述符
EACCES 地址被保护,用户的权限不足
EADDRNOTAVAIL 接口不存在或者绑定地址不是本地 UNIX 协议族,AF_UNIX
EFAULT my_addr 指针超出用户空间 UNIX 协议族,AF_UNIX
EINVAL 地址长度错误,或者 socket 不是 AF_UNIX 族 UNIX 协议族,AF_UNIX
ELOOP 解析 my_addr 时符号链接过多 UNIX 协议族,AF_UNIX
ENAMETOOLONG my_addr 过长 UNIX 协议族,AF_UNIX
ENOENT 文件不存在 UNIX 协议族,AF_UNIX
ENOMEN 内存内核不足 UNIX 协议族,AF_UNIX
ENOTDIR 不是目录 UNIX 协议族,AF_UNIX

比如这样:

struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = INADDR_ANY;
    
    if (bind(sfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) 
    {perror("bind");
        exit(1);
    }

listen

int listen(int sockfd, int backlog);

listen()函数将 sockfd 标记为被动打开的套接字,并作为 accept 的参数用来接收到达的连接请求。

  • sockfd 是一个套接字类型的文件描述符,具体类型为 SOCK_STREAM 或者 SOCK_SEQPACKET。
  • backlog 参数用来描述 sockfd 的等待连接队列能够达到的最大值。当一个请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,或者如果底层协议支持重传(比如 tcp 协议), 本次请求会被丢弃不作处理,在下次重试时期望能连接成功(下次重传的时候队列可能已经腾出空间)。

说起这个 backlog 就有一点儿历史了,等下文描述。

  • errno
含义
EADDRINUSE 另一个套接字已经绑定在相同的端口上。
EBADF 参数 sockfd 不是有效的文件描述符。
ENOTSOCK 参数 sockfd 不是套接字。
EOPNOTSUPP 参数 sockfd 不是支持 listen 操作的套接字类型。

connect

声明如下

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

参数说明如下

  • sockfd 是系统调用 socket() 返回的套接字文件描述符。
  • serv_addr 是 保存着目的地端口和 IP 地址的数据结构 struct sockaddr_in。
  • addrlen 设置 为 sizeof(struct sockaddr_in)

errno
connect 函数在调用失败的时候返回值 -1,并会设置全局错误变量 errno。

含义
EBADF 参数 sockfd 非合法 socket 处理代码
EFAULT 参数 serv_addr 指针指向无法存取的内存空间
ENOTSOCK 参数 sockfd 为一文件描述词,非 socket。
EISCONN 参数 sockfd 的 socket 已是连线状态
ECONNREFUSED 连线要求被 server 端拒绝。
ETIMEDOUT 企图连线的操作超过限定时间仍未有响应。
ENETUNREACH 无法传送数据包至指定的主机。
EAFNOSUPPORT sockaddr 结构的 sa_family 不正确。

accept

函数声明

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

参数说明
sockfd 是由 socket 函数返回的套接字描述符,参数 addr 和 addrlen 用来返回已连接的对端进程(客户端)的协议地址。如果我们对客户端的协议地址不感兴趣,可以把 arrd 和 addrlen 均置为空指针。

返回值

成功时,返回非负整数,该整数是接收到套接字的描述符;出错时,返回-1,相应地设定全局变量 errno。

含义
EBADF 非法的 socket
EFAULT 参数 addr 指针指向无法存取的内存空间
ENOTSOCK 参数 s 为一文件描述词,非 socket
EOPNOTSUPP 指定的 socket 并非 SOCK_STREAM
EPERM 防火墙拒绝此连线
ENOBUFS 系统的缓冲内存不足
ENOMEM 核心内存不足

特别需要说明下的是,这个 accept 是一个阻塞式的函数,对于一个阻塞的套套接字,一直阻塞,或者返回一个错误值,对于非阻塞套接字。accept 有可能返回 -1,但是如果 errno 的值为,EAGAIN 或者 EWOULDBLOCK,此时需要重新调用一次 accept 函数。

send_recv send 和 recv 函数

函数申明如下

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);
  • sockfd:套接字
  • buf : 待发送或者接收的缓存
  • len : 如果是 recv 指期望接收的长度,如果是 send 指要发送的长度。
  • flags : 标志位,取值如下表:
flags 说明 recv send
MSG_DONTROUTE 绕过路由表查找
MSG_DONTWAIT 仅本操作非阻塞
MSG_OOB 发送或接收带外数据
MSG_PEEK 窥看外来消息
MSG_WAITALL 等待所有数据

errno

含义 
EAGAIN 套接字已标记为非阻塞,而接收操作被阻塞或者接收超时 
EBADF sock 不是有效的描述词 
ECONNREFUSE 远程主机阻绝网络连接 
EFAULT 内存空间访问出错 
EINTR 操作被信号中断 
EINVAL 参数无效 
ENOMEM 内存不足 
ENOTCONN 与面向连接关联的套接字尚未被连接上 
ENOTSOCK sock 索引的不是套接字 当返回值是 0 时,为正常关闭连接;

当返回值为 - 1 时是不是一定就错误了,当返回值为 0 时该怎么做呢?

  • 如何正确判断一个对端已经关闭了连接?
/* 客户端设置非阻塞,然后判断链接是否成功 */
int SocketConnectWithTimeout
(
    int                 mySocket,          
    struct mySocketaddr     *adrs,        
    int                 adrsLen,        
    struct timeval      *timeVal        
)
{
    int     flag;
    fd_set  writeFds;
    int     remotPeerAdressLen;
    struct  mySocketaddr remotPeerAdress;
    
    if(timeVal == NULL)
    {return (connect(mymySocket, adrs, adrsLen));
    }
        
    flag = fcntl(mySocket, F_GETFL, 0); 
    fcntl(mySocket, F_SETFL, flag | O_NONBLOCK);// 修改当前的 flag 标志为给阻塞
    
    // 对于非阻塞式套接字,如果调用 connect 函数会之间返回 -1(表示出错),且错误为 EINPROGRESS,表示连接建立
    if(connect(mySocket, adrs, adrsLen) < 0)
    {// 当使用非阻塞模式的时候,如果链接没有被立马建立,则 connect()返回 EINPROGRESS
        if(errno == EINPROGRESS)
        {
        //select 是一种 IO 多路复用机制,它允许进程指示内核等待多个事件的任何一个发生,并且在有一个或者多个事件发生或者经历一段指定的时间后才唤醒它。//connect 本身并不具有设置超时功能,如果想对套接字的 IO 操作设置超时,可使用 select 函数。此时我们使用不断的检测 writeFds 来判断链接的建立?FD_ZERO(&writeFds);
            FD_SET((unsigned int)mySocket, &writeFds); 
    
            if(select(FD_SETSIZE, (fd_set *)NULL, &writeFds, (fd_set *)NULL, timeVal) > 0)
            {//select()成功了,查看 mySocketet 是否可写(关键)if (FD_ISSET ((unsigned int)mySocket, &writeFds))
                {// 已经可写了,此时我们要通过使用 getpeername()判断是否真正的链接成功,如果返回值不是 -1;// 说明 connect()成功了。remotPeerAdressLen = sizeof (remotPeerAdress);
                    if(getpeername (mySocket, &remotPeerAdress, &remotPeerAdressLen) != ERROR)
                    {return OK;}
                    else
                    {return ERROR;}
                }
            }
        }
        else
        {return ERROR;}
    }
 
    fcntl(mySocket, F_SETFL, flag);// 恢复标志位为阻塞
}
  • 1. 将打开的 socket 设为非阻塞的, 可以用 fcntl(socket, F_SETFL, O_NDELAY)完成(有的系统用 FNEDLAY 也可).

    2. 发 connect 调用, 这时返回 -1, 但是 errno 被设为 EINPROGRESS, 意即 connect 仍旧行还没有完成. 

    3. 将打开的 socket 设进被监视的可写 (注意不是可读) 文件集合用 select 进行监视, 如果可写用 getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int)); 来得到 error 的值, 如果为零, 则 connect 成功.

正文完
 0