UNP学习笔记1——基本TCP套接字编程

时间:2021-08-16 00:44:56

1 套接字地址结构

大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。每个协议族都定义了自己的套接字结构。这些套接字的结构以sockaddr_开头,以每个协议族唯一的后缀名结尾。

1.1 IPV4套接字地址结构

IPV4套接字结构通常被称为网际套接字结构,以sockaddr_in命名,定义在<netinet/in.h>头文件中。

struct sockaddr_in {

  uint8_t  sin_len;  //数据结构长度(16)
  sa_family_t  sin_family;  //AF_INET

  in_port_t  sin_port;  //16位TCP或UDP端口号,采用网络字节序
  struct in_addr  sin_addr;  //32位IPV4地址,采用网络字节序
  char sin_zero[8];  //保留
};

struct in_addr {

  in_addr_t  s_addr;  //32位IPV4地址,采用网络字节序
};
  • POSIX规范只需要sin_family、sin_addr和sin_port三个字段。
  • 长度字段sin_len是为了增加对OSI协议的支持才添加的,有了长度字段可以简化长度可变套接字地址结构的处理。除非涉及路由套接字,平时我们无需设置和检查它。
  • IPV4地址和TCP/UDP端口号在套接字地址结构中都是以网络字节序来存储。
  • 32位IPV4地址有两种不同的访问方法:server.sin_addr将以in_addr结构引用其中32位IPV4地址;而server.sin_addr.s_addr将以in_addr_t(通常是一个32位无符号整数)引用一个32位IPV4地址。因此应当正确使用IPV4地址,尤其是在函数引用时。因为编译器对传递结构和传递整数的处理是完全不同的。
  • sin_zero字段未使用,置为0
  • 套接字地址结构仅在本机上使用,结构本身并不在主机之间传递。

1.2 通用套接字地址结构

套接字的地址结构总是以引用(即指向该结构的指针)的形式传给套接字函数因此需要一个通用的套接字地址结构来处理任何协议族的情况。

#include <sys/socket.h>

struct sockaddr {

  uint8_t  sa_len;  
  sa_family_t  sa_family;  //地址族:AF_XXX
  char  sa_data[14];  //特定协议定义的地址类型
};

这些通用套接字地址结构的唯一作用就是对指向特定协议的套接字地址结构指针进行强制类型转换。

1.3 IPV6套接字地址结构

#include <netinet/in.h>

struct in6_addr {

uint8_t  s6_addr[16];  //128位IPV6地址,采用网络字节序
};

#define SIN6_LEN  //required foe compile-time tests

struct sockaddr_in6 {   uint8_t  sin6_len;  //该结构长度(28)   sa_family_t  sin6_family;  //AF_INET6   in_port_t  sin6_port;  //transport layer port#,采用网络字节序   uint32_t  sin6_flowinfo;  //流信息,未定义   struct in6_addr  sin6_addr;  //IPV6地址,采用网络字节序   uint32_t  sin6_scope_id;  //set of interfaces for a scope };
  • 如果系统支持套接字地址结构中的长度字段,那么常值SIN6_LEN必须被定义
  • IPV6地址族为AF_INET6,IPV4地址族为AF_INET
  • 结构中字段的先后顺序经过编排,保证64位对齐
  • sin6_flowinfo字段分成两个字段:低序20位是流标(flow label),高序12位保留
  • 对于具备范围的地址(scoped address),sin6_scope_id标识其范围(scope),最常见的是链路局部地址的接口索引

1.4 套接字地址结构的比较

UNP学习笔记1——基本TCP套接字编程

 

1.5 字节序函数

网络协议使用大端字节序来传输多字节字段。

大端字节序和小端字节序的转换函数如下:

#include <netinet/in.h>

/*主机字节序转网络字节序*/
uint16_t htons(uint16_t host16value);
uint32_t htonl(uint32_t host32value);

/*网络字节序转主机字节序*/
uint16_t ntohs(uint16_t net16value);
uint32_t ntohl(uint32_t net32value);

函数名字中,h代表host,n代表network,s代表short,l代表long。

1.6 字节操纵函数

#include <strings.h>

void bzero(void *dest, size_t nbytes);  //把目标字节串中指定数目的字节置为0
void bcopy(const void *src, void *dest, size_t nbytes);  //指定数目的字节拷贝
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);  /*返回:相等则为0,否则为非0*/

void *memset(void *dest, int c, size_t len);  //把目标字节串指定数目的字节置为值c
void *memcpy(void *dest, const void *src, size_t nbytes);  //类似bcopy,不过两个指针参数的顺序是相反的
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);  /*返回:相等为0,否则<0或>0*/

memcpy类似bcpy,不过两个指针参数的顺序是反的,memcpy遵循C语言的赋值顺序:dest=src。但是当源字节串与目标字节串重叠时,bcpy能正确处理,但是memcpy的结果不可知。

memcmp比较两个字节串,相等返回0,不等返回非0,是大于0还是小于0取决于第一个不等的字节。

1.7 地址转换函数

下面两个函数用于IPV4地址在点分十进制和32位网络字节序之间转换:

#include <arpa/inet.h>

int inet_aton(const char *strptr, struct in_addr *addrptr);  /*返回:若字符串有效则为1,否则为0*/
char *inet_ntoa(struct in_addr *inaddr);  /*返回:一个点分十进制字符串的指针*/

下面两个新函数,对于IPV4和IPV6都适用:

#include <arpa/inet.h>

int inet_pton(int family, const char *strptr, void *addrptr);  /*返回:成功为1,若输入不是有效的表达格式则为0,出错则为-1*/
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);  /*返回:成功则返回指向结果的指针,出错则为NULL*/

这两个函数的参数family可以是AF_INET或AF_INET6

inet_pton函数尝试转换由strptr指向的字符串,并通过addrptr指针存放二进制结果。inet_ntop进行相反的转换,len参数是目标存储单元的大小,以免该函数溢出调用者的缓冲区。如果len太小不能完整存放结果,则返回一个空指针,并置errno为ENOSPEC。

inet_ntop函数的strptr参数不能为空指针。调用者必须为目标存储单元分配内存并指定其大小。调用成功时,这个指针就是该函数的返回值。

 2 基本TCP套接字编程

本章讲解一个完整的TCP客户/服务器程序所需要的基本套接字函数。

2.1 socket函数

#include <sys/socket.h>

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

/*返回:成功返回非负描述符,出错-1*/

其中,family表示协议族,包括AF_INET、AF_INET6、AF_LOCAL(Unix域协议)、AF_ROUTE(路由套接字)、AF_KEY(密钥套接字),该参数也往往被称为协议域。type参数指明套接字类型,包括SOCK_STREAM(字节流套接字)、SOCK_DGRAM(数据报套接字)、SOCK_SEQPACKET(有序分组套接字)、SOCK_RAW(原始套接字)。protocol参数应设为某个协议类型常值,或者设为0,以选择所给定的family和type组合系统默认参数值。

UNP学习笔记1——基本TCP套接字编程

socket函数在成功后返回一个小的非负整数值,它与文件描述符类似,称为套接字描述符,简称sockfd。为了得到这个套接字描述符,我们制定了协议族和套接字类型,但是并没有指定本地协议地址或远程协议地址。

2.2 connect函数

TCP客户端用connect函数来建立与TCP服务器的连接。

#include<sys/socket.h>

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

/*返回:成功为0,出错为-1*/

 

第二个和第三个参数分别指向一个套接字地址结构指针和该结构的大小。

客户端在调用connect函数之前不用必须调用bind函数,因为内核会确定源IP地址,并选择一个临时端口作为源端口。

若是TCP套接字,调用connect函数将激发TCP的三路握手过程,而且仅在建立成功或出错之后才返回,其中出错返回可能有以下几种情况:

  1. TCP客户端没有收到SYN分节的响应,则返回ETIMEOUT错误。
  2. 若服务器对客户SYN的响应是RST(复位),则表明服务器在该端口上没有进程在等待连接(或者服务器主动取消连接),这是一种硬错误,返回ECONNREFUSED错误。
  3. 若客户端在发送SYN路径上某个路由器上引发了一个路径不可达的ICMP错误,这是一种软错误。客户端进程保存该信息,并在固定间隔内重复发送SYN,达到规定时间之后则返回EHOSTUNREACH或ENETUNREACH错误。

2.3 bind函数

bind函数把一个本地协议地址赋予一个套接字。

#include <sys/socket.h>

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

/*返回:成功为0,出错为-1*/

第二个和第三个参数分别指向一个套接字地址结构指针和该结构的大小。对于TCP,调用bind函数可以指定一个端口号,或者指定一个IP地址,也可以两者都指定或者都不指定。服务器在启动时捆绑它们的众所周知接口,进程可以把一个特定的IP地址绑定到它的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一。

2.4 listen函数

listen函数仅由TCP服务器调用,它做两件事情:

  1. 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说默认是一个主动发起connect的套接字。listen函数将一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求。
  2. 函数第二个参数规定了内核应该为相应套接字的最大连接个数。
#include <sys/socket.h>

int listen(int sockfd, int backlog);

/*返回:若成功返回0,出错为-1*/

 对于理解backlog函数,必须认识到内核为任何一个监听的套接字维护两个队列:

  • 未完成连接队列:服务器收到客户端的SYN,等待完成三次握手过程,此时套接字处于SYN_RCVD状态。
  • 已完成连接队列:已完成三次握手,套接字出ESTABLISHED状态。
  • 两个队列之和不得超过backlog。

2.5 accept函数

#include <sys/socket.h>

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

/*成功返回非负描述符,出错返回-1*/

accept函数由TCP服务器调用,用于从已完成连接队列对头返回下一个已完成的连接。如果已完成的队列为空,那么进程就被投入休眠(假定套接字为默认阻塞模式)。

UNP学习笔记1——基本TCP套接字编程

参数cliaddr和addrlen用来返回已连接的客户端的协议地址。addrlen是值-结果参数;调用前,我们将由*addrlen所指向的整数置为*cliaddr所指向的套接字结构的长度,返回时,该整数值有内核存放在该套接字地址结构的确切字节数。

如果accept成功,其返回一个内核生成的全新套接字描述符,成为已连接套接字描述符。而accept函数的第一个参数成为监听套接字描述符。一个服务器通常只创建一个监听套接字,它在服务器程序运行周期内一直存在。而当服务器完成对客户的服务时,其对应的已连接套接字就会被关闭。

2.6 close函数

#include <unistd.h>

int close(int sockfd);

/*返回:成功则为0,出错则为-1*/

close一个套接字的默认行为是将它标记为已关闭状态,然后立即返回到调用进程,之后该进程就不能再使用这个套接字描述符了。而TCP之后会尝试发送已排队等待的,将要发送到对端的数据,发送完毕之后发生的是正常的TCP连接终止序列。