套接字编程

套接字编程

套接字地址结构

  1. IPv4套接字地址结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct in_addr{
    in_addr_s_addr; //32位IPv4套接字地址
    };

    struct sockaddr_in{
    uint8_t sin_len; //结构体长度
    sa_family_t sin_family; //类型
    in_port_t sin_port; //端口号
    struct in_addr sin_addr; //32位IPv4地址
    char sin_zero[8];
    };
  2. 访问IPv4地址,使用serv.sin_addr引用

  3. 通用套接字地址结构,可以作为套接字函数的一个参数,通用套接字地址结构用来对特定于协议的套接字地址结构的指针执行类型强制转换

    1
    2
    3
    4
    5
    struct sockaddr{
    uint8_t sa_len;
    sa_family_t sa_family;
    char sa_data[14];
    };
  4. IPv6套接字地址结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct in6_addr{
    uint8_t s6_addr[16];
    };

    #define SIN6_LEN
    //如果系统支持套接字地址结构的长度字段,SIN6_LEN必须定义

    struct sockaddr_in6{
    uint8_t sin6_len; //
    sa_family_t sin6_family;
    in_port_t sin6_port;

    uint32_t sin6_flowinfo; //分为两个字段,低20位是流标,高12位保留
    struct in6_addr sin6_addr;

    uint32_t sin6_scope_id; //标识具备范围的地址,最常见的是链路局部地址的接口索引
    };
  5. 新的通用套接字地址结构

    1
    2
    3
    4
    5
    6
    //满足对其要求,且足够大能够容纳系统支持的任何套接字地址结构
    //必须强制类型转换成或复制到合适与ss_family字段给出的地址类型的套接字地址结构中,才能访问其他字段
    struct sockaddr_storage{
    uint_t ss_len;
    sa_family_t ss_family;
    };

key-value参数

  1. 从进程到内核传递套接字地址结构的函数有三个:bind、connect、sendto,这些函数的第一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小

    1
    2
    struct sockaddr_in serv;
    connect(sockfd, (SA *) &serv, sizeof(serv));
  2. 从内核到进程传递套接字地址结构的函数有四个:accept、recvfrom、getsockname、getpeername,有两个参数分别是指向某个套接字地址结构的指针和指向表示该结构大小的整型指针

    1
    2
    3
    4
    struct  sockaddr_un cli;
    socklen_t len;
    len=sizeof(cli);
    getpeername(unixfd, (SA *) &cli, &len);

字节排序函数

  1. 字符串CPU_VENDOR_OS是GNU的autoconf程序在配置本书的软件时确定的
  2. 主机字节序和网络字节序之间互转函数
    1
    2
    3
    4
    5
    6
    7
    #include <netinet/in.h>
    //返回网络字节序的值
    uint16_t htons(uint16_t host16bitvalue);
    uint32_t htonl(uint32_t host32bitvalue);
    //返回主机字节序的值
    uint16_t ntohs(uint16_t net16bitvalue);
    uint32_t ntohl(uint32_t net32bitvalue);

字节操纵函数

  1. 以空字符结尾的C字符串在<string.h>中定义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <strings.h>

    void bzero(void (*dest, size_t nbytes); //将制定数目的字节置为0,经常用来将一个套接字地址结构初始化为0
    void bcopy(const void *src, void *dest, size_t nbytes);
    //将制定数目的字节源字节串移到目标字节串,可以处理字节串重叠
    int bcmp(const void *ptrl, const void *ptr2, size_t nbytes); //比较两个任意的字节串,相同返回0

    #include <string.h>

    void *menset(void *dest, int c, size_len); //目标字节串指定数目的字节置为值c
    void *memcpy(void *dest, const void *src, size_t nbytes); //源字节串和目标字节串重叠需要调用memmove
    int memcmp(const void *ptrl, const void *ptr2, size_t nbytes); //比较两个字符串

地址转换函数

  1. 在ASCII字符串与网络字节序的二进制之间转换网际地址

    1
    2
    3
    4
    5
    6
    7
    #include <arap/inet.h>

    int inet_aton(const char *strptr, struct in_addr *addrptr);
    //将strptr所指的C字符串转换为一个32位的网络字节序的二进制,通过指针addrptr来存储,成功返回1
    in_addr_t inet_addr(const char *strptr); //作用同上,出错时函数返回INADDR_NONE
    char *inet_ntoa(struct in_addr inaddr);
    //将32位网络字节序二进制地址转换成相应的点分十进制数串
  2. 同时使用IPv4和IPv6的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <arap/inet.h>

    /*family参数可以是AF_INET也可以是AF_INET6,如果是不支持的地址组被作为参数,返回错误将errno置为eafnosupport*/
    int inet_pton(int family, const char *strptr, void *addrptr);
    //将strptr所指的C字符串转换为一个32位的网络字节序的二进制,通过指针addrptr来存储,成功返回1
    const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
    /*从数值格式转换到表达格式,len参数是目标存储单元的大小,定义如下,如果len太小不足以容纳表达式结果,则返回空指针,errno置为ENOSPC*/

    #include <netinet/in.h>
    #define INET_ADDRSTRLEN 16
    #define INET6_ADDRSTRLEN 46
  3. char *sock_ntop(const struct sockaddr *sockaddr, socklen_t addrlen);
    查看套接字地址的指针为参数,查看结构的内部,然后调用适当的函数返回地址的表达格式

  4. readline函数每读一个字节就要调用一次系统read函数,非常低效。如果为了解决这个问题改用标准I/O函数库十分危险,不能检查网络数据传送的错误也不能修正和恢复

基于TCP的套接字编程

socket函数

  1. 进程必须做的第一件事就是调用socket函数,指定通信协议类型
    1
    2
    3
    4
    5
    #include <sys/socket.h>
    //成功返回非负套接字描述符sockfd,出错返回-1
    int socket(int family, int type, int protocol);
    //family参数指明协议族,type指明套接字类型,protocol指明协议类型常值
    //若protocol设为0,选择给定family和type组合的系统默认值

connect函数

1
2
3
4
5
#include <sys/socket.h>
//connect函数建立与TCP服务器的连接,成功返回0,出错返回-1
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
//sockfd是socket套接字描述符,*servaddr指向套接字地址结构,addrlen是这个结构的大小
//套接字地址结构必须包含服务器的IP地址和端口号
  1. connect函数将会激发TCP的三次握手
  2. 如果三次握手中TCP客户端没有接收到SYN分节,返回ETIMEOUT
  3. 若是对客户的SYN响应是RST,马上返回ECONNREFUSED
  • 目的端口的SYN到达,端口上没有正在监听的服务器
  • TCP想要取消一个已有连接
  • TCP接收到一个根本不存在的连接上的分节
  1. 如果客户发出的SYN在中间的某个路由器引发一个ICMP错误,客户主机内核保存该消息,按照一定的时间间隔继续发送SYN,在规定时间后仍未收到回应,将保存的信息作为EHOSTUNREACH或ENETUREACH错误返回给进程

bind函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/socket.h>
//函数将协议地址赋给一个套接字,协议地址是套接字端口号和IP地址类型的组合
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
//*myaddr是指向协议的地址结构的指针,addrlen是该地址结构的长度

//如果指定端口号为0,内核在bind被调用时,选择一个临时端口
//如果指定IP地址为通配地址,内核等到套接字已连接或套接字发送数据报时才选择一个本地IP地址

struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr=htonl(INAADR_ANY);
//IPv4通配地址由常值INAADR_ANY指定

struct soackaddr_in6 serv;
serv.sin6_addr=in6addr_any;
//系统预先分配in6addr_any变量,并将其初始化为常值IN6ADDR_ANY_INIT,头文件<netinet/in.h>中包含in6addr_any的extern声明
  1. 如果内核为套接字选择一个临时端口号,函数bind不返回所选择的临时端口,必须调用getsockname来返回协议地址以得到内核选择的临时端口值

listen函数

  1. 当socket创建一个套接字时,被假设为一个主动套接字,listen函数把一个未连接套接字转换成一个被动套接字

    1
    2
    3
    4
    #include <sys/socket.h>
    int listen(int sockfd, int backlog);
    //第二个参数规定内核为相应套接字排队的最大连接数,防止套接字接受新的连接
    //成功返回0,出错返回-1
  2. 调用listen函数导致套接字从CLOSED到LISTEN

  3. 参数backlog参数被规定为两个队列(未完成连接队列和已完成队列)总和的最大值,可以给这个参数添加一个模糊因子,不能设为0
  4. 未完成链接的队列的分节存留的时间是RTT
  5. 三次握手完成之后,在服务器调用accept之前到达的数据应由服务器TCP排队,最大数据量为相应已连接套接字的接收缓冲区大小

accept函数

1
2
3
4
5
6
7
#include <sys/socket.h>
//从已完成连接队列的列头返回下一个已完成连接,如果队列为空,进程投入睡眠
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
//*important*第一个参数是监听套接字描述符,返回值是已连接套接字描述符
//调用前将addrlen参数置为cliaddr所指的套接字地址结构的长度,返回时整数值即为内核存放该套接字地址结构内的确切字节数
//调用成功返回非负描述符,出错返回-1
//返回的非负描述符代表客户的TCP连接,

fork和exec函数

1
2
#include <unistd.h>
pid_t fork(void); //
  1. 调用fork函数在父进程中返回一次,返回值是子进程的进程ID,子进程中返回0,返回值的作用是告诉子进程当前进程是子进程还是父进程
  2. 子进程可以调用getppid获得父进程的进程ID,而父进程可以有很多子进程,无法一一查验子进程ID
  3. fork函数有两个用法:
  • 进程创建当前进程的副本即子进程
  • 进程想要执行另一个程序,则调用fork创建一个新进程,然后其中一个副本调用exec将自身体换成新的程序
1
2
3
4
5
6
7
8
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ..., /*(char *) 0*/);
int execv(const char *pathname, char *const *argv[]);
int execle(const char *pathname, const char *arg0, ..., /*(char *) 0, char *const envp[]*/);
int execve(const char *pathname, char *const argv[], char *const envp[]);
//execve函数是内核的系统调用,其他五个都是库函数
int execlp(const char *pathname, const char *arg0, ..., /*(char *) 0*/);
int execvp(const char *pathname, char *const argv[]);
  1. 这些函数只有在出错时才返回到调用者
  2. 待处理程序文件是由文件名或是路径名指定的
  3. 程序的参数是一一列出还是由一个指针数组来引用的
  4. 把调用进程的环境传递给新程序还是给新程序指定新的环境

并发服务器

  1. 一个新的连接建立,accept返回,服务器接着调用fork,然后由子程序服务客户(已连接套接字connfd),父进程关闭已连接套接字,此时父进程通过监听套接字listenfd等待下一个连接

close函数

1
2
#include <unistd.h>
int close(sockfd);
  1. close函数关闭套接字,当套接字描述符的引用计数大于1,则调用close的作用是将引用计数-1,直到引用计数为0,关闭套接字
  2. 如果确定要关闭套接字,可以使用shutdown来代替close关闭套接字

getsockname函数和getpeername函数

1
2
3
4
5
#include <sys/socket.h>
//返回套接字关联的本地协议地址或者返回套接字关联的外地协议地址
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
//返回客户的IP地址和端口号
  1. 在没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号
  2. 以端口号0调用bind后,getsockname用于返回有内核赋予的本地端口号
  3. 用通配IP地址调用bind的TCP服务器,getsockname函数返回由内核赋予该连接的本地IP地址
  4. 当服务器是由调用过accept的进程通过调用exec执行程序时,能够获取客户身份的唯一途径就是通过getpeername
WhitneyLu wechat
Contact me by scanning my public WeChat QR code
0%