socket编程常用API

文章目录

  • socket编程常用API汇总
    • 1.socket() 创建套接字
    • 2.bind() 绑定套接字与网络地址(把一个本地协议地址赋予一个套接字)
        • 1.通用地址结构体的定义:
        • 2.特殊地址结构体 —— IPv4 地址结构体:
        • 3.特殊地址结构体 —— IPv6 地址结构体:
        • 4.特殊地址结构体 ——UNIX 域地址结构体:
    • 3.connect() 建立连接
    • 4.listen() 设置监听端口
    • 5.accept() 接受TCP连接
    • 6.`recv(), read(), recvfrom() 数据接收` | `send(), write(), sendto() 数据发送`
        • 1)recv()和send()
        • 2)recvfrom()和sendto()
    • 8.close(), shutdown() 关闭套接字
        • **how**
        • **shutdown与close的区别**:
    • 9.`inet_pton()将文本地址转化为二进制地址` | `inet_ntop将二进制地址转化为文本地址`
    • 10.多路复用
        • 1)select
        • select的优缺点
        • 2)poll
        • poll的优缺点
    • 11.字节序转换htonl、htons、ntohl、ntohs
        • poll的优缺点
    • 11.字节序转换htonl、htons、ntohl、ntohs

socket编程常用API汇总

典型的UDP客户/服务器程序的函数调用:

socket编程常用API_第1张图片
典型的TCP客户/服务器程序的函数调用:
socket编程常用API_第2张图片
服务器:

  1. 创建一个监听套接字 socket()

    设置套接字属性(可以设置,也可以忽略) setsockopt()

  2. 初始化地址结构体

    (协议簇,端口号,服务器ip地址)

  3. 绑定地址结构体 bind()

  4. 开启监听 listen()

  5. 接收连接请求accept(),建立连接(得到通信套接字)

  6. 数据收发 收recv/read 发send/write

  7. 关闭连接socket套接字 close(), shutdown()

客户端:

  1. 创建一个通信套接字 socket()

    设置套接字属性(可以设置,也可以忽略) setsockopt()

  2. 初始化地址结构体

(协议簇,端口号,设置服务器的ip地址)

  1. 请求连接 connect()

  2. 数据收发 收recv/read 发send/write

  3. 关闭连接socket套接字 close(), shutdown()

TCP可靠传输
socket编程常用API_第3张图片

1.socket() 创建套接字

功能
    创建套接字
头文件
    #include           /* See NOTES */
    #include 
函数原型
	int socket(int domain, int type, int protocol);
参数
    domain:域
        AF_INET/PF_INET: 网际协议
        AF_UNIX/PF_UNIX:本地协议,可写AF_LOCAL/PF_LOCAL
    type:类型
        SOCK_STREAM:流式套接字
        SOCK_DGRAM:数据报套接字
    protol:协议
    	一般为 0
返回值
    成功 待连接套接字(非负描述符)
    失败 -1
备注
    在网际协议中,选择流式套接字就代表 TCP 协议,选择数据包套接字就代表 UDP 协议,第三个参数 protocol 一般都不用

2.bind() 绑定套接字与网络地址(把一个本地协议地址赋予一个套接字)

功能
    绑定套接字与网络地址
头文件
    #include           /* See NOTES */
    #include 
函数原型
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
    sockfd:待连接套接字
    addr:包含本地地址(IP+PORT)的通用地址结构体的指针
    addrlen:地址结构体大小
返回值:
    成功:0
    失败:-1
1.通用地址结构体的定义:
通用地址结构体的定义:
struct sockaddr
{
    sa_family_t sa_family;
    char    sa_data[14];
};
2.特殊地址结构体 —— IPv4 地址结构体:
特殊地址结构体 —— IPv4 地址结构体:
struct sockaddr_in
{
    u_short sin_family;// 地址族
    u_short sin_port;// 端口
    struct in_addr sin_addr;// IPV4 地址
    char sin_zero[8];
};
struct in_addr
{
    in_addr_t s_addr;// 无符号 32 位网络地址
};
3.特殊地址结构体 —— IPv6 地址结构体:
特殊地址结构体 —— IPv6 地址结构体:
struct sockaddr_in6
{
    u_short sin6_family;// 地址族
    __be16 sin6_port;// 端口
    __be32 sin6_flowinfo; // 流信息
    struct in6_addr sin6_addr; // IPv6 地址
    __u32 sin6_scope_id;
};
4.特殊地址结构体 ——UNIX 域地址结构体:
特殊地址结构体 ——UNIX 域地址结构体:
struct sockaddr_un
{
    u_short sun_family;// 地址族
    char sun_path[108];// 套接字文件路径
};

3.connect() 建立连接

功能:
    连接对端监听套接字
头文件
    #include           /* See NOTES */
    #include 
函数原型:
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
    sockfd:客户端套接字描述符
    addr:包含服务器IP地址和端口号的套接字地址结构
    addrlen:套接字地址结构体大小
返回值:
    成功:0
    失败:-1

调用connect前不必非得调用bind,如果没有bind,内核会确定源IP并选择一个临时端口作为源端口

如果是TCP套接字,调用connect将激发TCP三路握手过程,函数会阻塞进程,直到成功或出错才返回。出错可能有下列情况:

  1. 返回ETIMEDOUT错误:以4.4BSD为例,内核会先发送一个SYN,若无响应则等待6s后再发送一个,若仍无响应则等待24s后再发送一个。若总共等待了75s后仍未收到响应则返回该错误
  2. 返回ECONNREFUSED错误:服务器对客户响应一个RST,表明服务器在客户指定的端口上没有进程在等待与之连接(除此之外,产生RST还有两个条件:1)TCP想取消一个已有连接;2)TCP接收到一个根本不存在的连接上的分节)
  3. 返回EHOSTUNREACH或ENETUNREACH错误:客户发出的SYN在中间的某个路由引发一个“目的地
    不可达”ICMP错误,内核会报错该消息,并按情况1中的间隔继续发送SYN,若在规定时间内仍未收到响应,则把报错的信息作为这两种错误之一返回给进程

connect失败则该套接字不可再用,必须关闭,不能对这样的套接字再次调用connect函数。必须close后重新调用socket

4.listen() 设置监听端口

功能:
    将待连接套接字设置为监听套接字,并设置最大同时接收连接请求个数
头文件
    #include           /* See NOTES */
    #include 
函数原型:
    int listen(int sockfd, int backlog);
参数:
    sockfd:待连接套接字
    backlog:最大同时接收连接请求个数
返回值:
    成功:0,并将 sockfd 设置为监听套接字
    失败:-1
备注:
    由于历史原因,各种系统对 backlog 的理解并不一致,以 LINUX 为例,监听端能同时接收的最大连接请求个数为 backlog+4

listen做2件事:

  • 当socket创建一个套接字时,套接字被假设为一个主动套接字,listen将其转成一个被动套接字,指示内核应接受指向该套接字的连接请求
  • 第二个参数规定了内核应为相应套接字排队的最大连接个数

内核为任一给定的监听套接字维护两个队列,两个队列之和不超过backlog:

  • 未完成连接队列
  • 已完成连接队列

socket编程常用API_第4张图片

当进程调用accept时,如果已连接队列不为空,那么队头项将返回给进程,否则进程将投入睡眠,直到TCP在该队列中放入一项才唤醒它

不要把backlog指定为0,因为不同的实现对此有不同的解释。如果不想让任何客户连接到监听套接字上,那就关掉该监听套接字

设置backlog的一种方法是使用一个常值,但是增大其大小需要重新编译服务器程序;另一种方法是通过读取一个环境变量来设置该值,该环境变量具有一个默认值;(如果设定的backlog比内核支持的值要大,那么内核会悄然把所指定的偏大值截成自身支持的最大值)

SYN到达时,如果队列已满,TCP忽略该SYN分节:这么做是因为这种情况是暂时的,这种处理方式希望通过重传机制,在客户端下一次重传时,队列中已经存在可用空间。如果服务器立即响应RST,客户的connect调用就会立即返回一个错误,强制应用程序处理这种情况。另外,客户也无法区分RST是意味着“该端口没有服务器在监听”还是意味着“队列已满”

5.accept() 接受TCP连接

功能:
    等待对端连接请求
头文件
    #include           /* See NOTES */
    #include 
函数原型:
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
    sockfd:监听套接字描述符
    addr:已连接的对端客户的套接字地址结构
    addrlen:调用时指示内核能向cliaddr写入的最大字节数,返回时内核指明实际写入的字节数
返回值:
    成功:已连接套接字(非负整数)
    失败:-1

accept用于从已完成连接队列队头返回下一个已完成连接,如果已完成连接队列为空,那么进程被投入睡眠

如果对返回客户协议地址不感兴趣,可以把cliaddr和addrlen均置为空指针

6.recv(), read(), recvfrom() 数据接收 | send(), write(), sendto() 数据发送

1)recv()和send()
功能:
    从 TCP 套接字接收数据/发生数据
头文件:
    #include 
    #include 
函数原型: 
    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:缓冲区大小
    flags:接收标志
    MSG_OOB:接收紧急(带外)数据
返回值:
    成功:已接收字节数
    失败:-1
备注:
    当 flags 为 0 时,recv 与 read 作用相同。

这两个函数与标准的read和write的不同在于第4个参数:

  • flag:或为0或为一下一个或多个常值的逻辑或:
  • MSG_DONTROUTE:告知内核目的主机在某个直接连接的本地网络上,因而无需执行路由表查找
  • MSG_DONTWAIT:在无需打开相应套接字的非阻塞标志下,把单个I/O操作临时指定为非阻塞
  • MSG_PEEK:允许我们查看已经可读取的数据,而且系统不在recv或recvfrom返回后丢弃这些数据
  • MSG_WAITALL:告知内核不要在尚未读入请求数目的字节之前让一个读操作返回(即使指定了这个标志,如果发生:a)捕获一个信号;b)连接被终止;c)套接字发生一个错误;那么相应的读函数仍有可能返回比所请求字节数要少的数据

flags参数在设计上存在一个基本问题:它是值传递的,而不是一个值-结果参数。因此它只能用于从进程向内核传递标志。内核无法向进程传递标志。对于TCP/IP协议这一点不成问题,因为TCP/IP几乎从不需要从内核向进程传回标志。然而随着OSI协议被加到4.3BSD Reno中,却提出了随输入操作向进程返送MSG_EOR标志的需求。4.3BSD Reno做出的决定是保持常用输入函数(recv和recvfrom)的参数不变,而改变recvmsg和sendmsg的msghdr结构。这个决定同时意味着如果一个进程需要由内核更新标志,它就必须调用recvmsg,而不是recv或recvfrom

2)recvfrom()和sendto()
功能:
    向 UDP 套接字发送数据/接收数据
头文件:
    #include 
    #include 
函数原型:
        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);
参数:
    sockfd:UDP 套接字
    buf:即将发送的数据
    len:数据的长度
    flags:发送标志,与函数 send 的 flags 完全一致
    dest_addr:对端网络地址
    addr_len:地址长度
返回值:
    成功:已发送字节数
    失败:-1
备注:
    当 dest_addr 为 NULL,addrlen 为 0 时,sendto 与 send 作用一致
  • recvfrom
    • src_addr:指向一个将由该函数在返回时填写数据报发送者的协议地址的套接字地址结构
    • addrlen(指针):在from指向的套接字地址结构中填写的字节数
  • sendto
    • dest_addr:指向一个含有数据报接收者的协议地址的套接字地址结构
    • addrlen(整数):指定to指向的套接字地址结构的大小

写一个长度为0的数据报是可行的。在UDP情况下,这会形成一个只包含一个IP首部(对于IPv4通常为20字节,对于IPv6通常为40字节)和一个8字节UDP首部而没有数据的IP数据报。这也意味着对于数据报协议,recvfrom返回0值是可接受的:它并不像TCP套接字上read返回0值那样表示对端已关闭连接。既然UDP是无连接的,因此也就没有诸如关闭一个UDP连接之类的事情

接收缓冲:UDP层中隐含有排队发生。事实上每个UDP套接字都有一个接收缓冲区,到达该套接字的每个数据报都进入这个套接字接收缓冲区。当进程调用recvfrom时,缓冲区中的下一个数据报以FIFO顺序返回给进程、这样,在进程能够读该套接字中任何已排好队的数据报之前,如果有多个数据报到达该套接字,那么相继到达的数据报仅仅加到该套接字的接收缓冲区中。这个缓冲区的大小是有限的

对于一个UDP套接字,如果其进程首次调用sendto时,它没有绑定一个本地端口,那么内核就在此时为它选择一个临时端口

socket编程常用API_第5张图片

圆点指明了客户发送UDP数据报时,必须指定或选择的4个值

  • 客户必须给sendto调用指定服务器的IP地址和端口号
  • 客户的IP地址和端口号可以(调用bind)指定也可以不指定
    • 如果客户没有捆绑具体的IP和端口号,内核会自动选择
      • 临时端口是在第一次调用sendto时一次性选定,不能改变
      • IP地址却可以随客户发送的每个UDP数据报而变动
    • 如果客户绑定了一个IP地址
      • 在这种情况下,如果内核决定外出数据报必须从另一个数据链路发出,IP数据报将会包含一个不同于外出链路IP地址的源IP地址

在一个未绑定(指定)端口号和IP地址的UDP套接字上调用connect时,会给该套接字指派一个IP地址和临时端口

TCP和UDP服务器上获取源IP源端口号目的IP目的端口号的方法:
socket编程常用API_第6张图片

  • 非连接状态下,同一套接字可以给多个服务器发送数据报
  • 服务器上同一套接字可以从若干不同客户端接收数据报
  • recvfrom和sendto都可以用于TCP,尽管通常没有理由这样做

8.close(), shutdown() 关闭套接字

close()

功能:
    断开本端连接套接字
头文件
    #include 
函数原型:
    int close(int fd);
参数:
    fd:已连接套接字
返回值:
    成功:0
    失败:-1
备注:
    同时断开读端和写端

close一个TCP套接字的默认行为是把套接字标记为关闭,立即返回调用进程,然后TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后是正常的TCP连接终止序列

close会将套接字描述符的引用计数减1,如果引用计数仍大于0,则不会引起TCP的四次挥手终止序列

shutdown()

功能:
    断开本端连接套接字
头文件
    #include 
函数原型:
    int shutdown(int sockfd, int how);
参数:
    sockfd:已连接套接字
    how:断开方式。
        SHUT_RD:关闭读端
        SHUT_WR:关闭写端
        SHUT_RDWR:同时关闭读写端
返回值:
    成功:0
    失败:-1
备注:
    在只关闭一端的时候,另一端可以继续使用。
how
  • SHUT_RD关闭连接的读这一半,套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数(对一个TCP套接字这样调用shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃)
  • SHUT_WR关闭连接的写这一半对于TCP,称为半关闭),套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。进程不能再对这样的套接字调用任何写函数
  • SHUT_RDWR连接的读半部和写半部都关闭。等价于调用2次shutdown,分别指定SHUT_RD与SHUT_WR
shutdown与close的区别
  1. 关闭套接字的时机不同
    • close把描述符的引用计数减1,仅在该计数变为0时才关闭套接字
    • shutdown不管引用计数就能激发TCP的正常连接终止序列
  2. 全关闭与半关闭
    • close终止读和写两个方向的数据传送
    • shutdown可以只关闭一个方向的数据传送(具体见上面的howto参数)

9.inet_pton()将文本地址转化为二进制地址 | inet_ntop将二进制地址转化为文本地址

inet_pton

功能:
	将文本地址转化为二进制地址
头文件:
    #include 
函数原型:
    int inet_pton(int af, const char *src, void *dst);
参数:
    af:地址族
    AF_INET:IPv4 地址
    AF_INET6:IPv6 地址
    src:指向“点分式”IPv4 或 IPv6 地址的指针,例如“192.168.1.100”
    dst:类型为 struct in_addr *或者 struct in6_addr *的指针
返回值:
    成功:1
    失败:0 代表地址与地址族不匹配,-1 代表地址不合法


inet_ntop

功能:
        将二进制地址转化为文本地址
头文件
         #include 
函数原型:
        const char *inet_ntop(int af, const void *src, char *dst, socklen_tsize);
参数:
    af:地址族。
    AF_INET:IPv4 地址
    AF_INET6:IPv6 地址
    src:类型为 struct in_addr *或者 struct in6_addr *的指针
    dst:地址缓冲区指针,缓冲区至少
    size:地址缓冲区大小,至少要 INET_ADDRSTRLEN 或者 INET6_ADDRSTRLEN 个字节
返回值:
    成功:dst
    失败:NULL

10.多路复用

1)select

实现监控多个文件描述符,通过委托内核监控读事件、写事件、异常事件

头文件
    #include 
函数原型:
    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
    nfds:所有正在监测的套接字的最大值加 1
    readfds:读就绪文件描述符集合
    writefds:写就绪文件描述符集合
    exceptfds:异常就绪文件描述符集合
    timeout:超时控制
返回值:
    成功:就绪文件描述符总数(当超时返回时为 0)
    失败:-1
备注:
    文件描述符集合操作函数:
    void FD_CLR(int fd, fd_set *set);
    int  FD_ISSET(int fd, fd_set *set);
    void FD_SET(int fd, fd_set *set);
    void FD_ZERO(fd_set *set);
  • 参数

    • nfds:指定待测试的描述符个数,值为待测试的最大描述符加1(参数名的由来)

    • readfds:读描述符集

    • writefds:写描述符集

    • exceptfds:异常描述符集。目前支持的异常条件只有两个:

      • 1)某个套接字的带外数据到达
      • 2)某个已设置为分组模式的伪终端存在可从其主端读取的控制状态信息
    • timeout:(告知)内核等待任意描述符就绪的超时时间,超时函数返回0

      • 永远等待下去:设为空指针
      • 等待一段固定时间
      • 立即返回(轮询):timeval结构中的时间设为0
      struct timeval{
          long tv_sec;    /* 秒 */
          long tv_usec;   /* 微妙 */
      };
      
  • 描述符集说明

    • 1)select使用的描述符集,通常是一个整数数组,其中每个整数中的一位对应一个描述符
    • 2)如果对三个描述符集中的某个不感兴趣,可以设为空指针,如果都设为空指针,会得到一个比Unix的sleep函数更精确的定时器(sleep以秒为最小单位)
    • 3)中定义的FD_SETSIZE常值是数据类型fd_set中的描述符总数,其值通常是1024,不过很少用到这么多描述符,maxfdp1参数迫使我们计算

操作描述符集:描述符集是“值-结果“参数,select调用返回时,结果将指示哪些描述符已就绪。函数返回后,使用FD_ISSET宏来测试fd_set数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清为0。因此,每次重新调用select函数时,都得再次把所有描述符集内所关心的位设置为1

void FD_ZERO(fd_set *fdset);            //清除fdset的所有位
void FD_SET(int fd,fd_set *fdset);      //打开fdset中的fd位
void FD_CLR(int fd,fd_set *fdset);      //清除fdset中的fd位
int FD_ISSET(int fd,fd_set *fdset);     //检查fdset中的fd位是否置位
select的优缺点
  • 优点
    • 跨平台支持好,目前几乎在所有平台上支持
  • 缺点
    • 最大的缺点是,进程打开的fd有限(由FD_SETSIZE和内核决定,一般为1024),这对于连接数量比较大的服务器来说根本不能满足(可以选择多进程的解决方案,虽然Linux上创建进程的代价比较小,但也不可忽视,加上进程间数据同步远比不上线程间同步的效率,所以也不是一种完美的方案)
    • 函数返回后需要轮询描述符集,寻找就绪描述符,效率不高
    • 用户态和内核态传递描述符结构时copy开销大

增大描述符集大小的唯一方法是:先增大FD_SETSIZE的值,再重新编译内核,不重新编译内核而改变其值是不够的

2)poll
头文件:
    #include 
函数原型:
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
    fds:监测文件描述符结构体数组
    nfds:数组元素个数
    timeout:超时控制
返回值:
    成功:若有就绪描述符则为其数目,若超时则为0
    失败:-1
  • 参数:

    • fdarray:指向pollfd数组的指针,每个pollfd结构包含了描述符及其相应事件

      struct pollfd
      {
          int fd;// 监测的文件描述符
          short events;// 监测的状态
          short revents;// 实际发生的状态
      };
      其中,监测的状态可以用以下宏来标记:
          POLLIN:读就绪
          POLLPRI:紧急数据读就绪
          POLLOUT:写就绪
          POLLRDHUP:对端已关闭或已关闭写端(仅对流式套接字有效)
          POLLERR:发生错误
      
  • nfds:pollfd数组的元素个数(即监视的描述符总数)

  • timeout:(告知)内核等待任意描述符就绪的超时时间,超时函数返回0

    • INFTIM(一个负值):永远等待下去
    • >0:等待一段固定时间
    • 0:立即返回(轮询)

如果不再关心某个特定描述符,可以把与之对应的pollfd结构的fd成员设置成一个负值。poll函数将忽略这样的pollfd结构的events成员,返回时将其revents成员的值置为0

poll的优缺点
  • 优点
    • 没有最大监视描述符数量的限制:分配一个pollfd结构的数组并把该数组中元素的数目通知内核成了调用者的责任。内核不再需要知道类似fd_set的固定大小的数据类型
  • 缺点
    • 和select一样,调用返回后需要轮询所有描述符来获取已经就绪的描述符
    • 用户态和内核态传递描述符结构时copy开销大

11.字节序转换htonl、htons、ntohl、ntohs

函数原型:
    uint32_t htonl(uint32_t hostlong);
    uint16_t htons(uint16_t hostshort);
    uint32_t ntohl(uint32_t netlong);
    uint16_t ntohs(uint16_t netshort);
参数:
    hostlong:主机字节序的长整型数据
    hostshort:主机字节序的短整型数据
    netlong: 网络字节序的长整型数据
    netshort:网络字节序的短整型数据
返回值:
	对应的字节序数据
poll的优缺点
  • 优点
    • 没有最大监视描述符数量的限制:分配一个pollfd结构的数组并把该数组中元素的数目通知内核成了调用者的责任。内核不再需要知道类似fd_set的固定大小的数据类型
  • 缺点
    • 和select一样,调用返回后需要轮询所有描述符来获取已经就绪的描述符
    • 用户态和内核态传递描述符结构时copy开销大

11.字节序转换htonl、htons、ntohl、ntohs

函数原型:
    uint32_t htonl(uint32_t hostlong);
    uint16_t htons(uint16_t hostshort);
    uint32_t ntohl(uint32_t netlong);
    uint16_t ntohs(uint16_t netshort);
参数:
    hostlong:主机字节序的长整型数据
    hostshort:主机字节序的短整型数据
    netlong: 网络字节序的长整型数据
    netshort:网络字节序的短整型数据
返回值:
	对应的字节序数据

你可能感兴趣的:(Linux-c,网络,tcp/ip,网络协议)