接触网络编程一年多了,最近在系统的学习vnp两本书,对基础知识作一些总结,但愿理解的更透彻清晰,但愿能有更多的沉淀。编程
1.套接口地址安全
针对IPv4和IPv6地址族,分别定义了两种类型的套接口地址:sockaddr_in和sockaddr_in6,两种套接口地址结构以下所示: 服务器
/* IPv4地址族套接口地址结构 */ struct in_addr { in_addr_t s_addr; /* IPv4地址,网络序存储 */ } struct sockaddr_in { uint8_t sin_len; /* 结构体大小 */ sa_family_t sin_family; /* 地址族:AF_INET */ in_port_t sin_port; /* 16位端口号,网络序存储 */ struct in_addr sin_addr; /* IPv4地址,网络序存储 */ char sin_zero[8] /* 保留字段,未使用 */ }; /* IPv6地址族套接口地址结构 */ struct in6_addr { uint8_t s6_addr[16]; /* IPv6地址,网络序存储 */ } struct sockaddr_in6 { uint8_t sin6_len; /* 结构体大小 */ sa_family_t sin6_family; /* 地址族:AF_INET */ in_port_t sin6_port; /* 16位端口号,网络序存储 */ uint32_t sin6_flowinfo /* 流标记或优先级,网络序存储 */ struct in6_addr sin6_addr; /* IPv4地址,网络序存储 */ };
这两个套接口地址结构在/netinet/in.h头文件中定义,上面结构体的描述和头文件中的定义有一些差异,好比在头文件中sa_family_t成员经过宏定义在了公共部分,另外表示结构体大小的sin_len和sin6_len成员,在某些实现中没有定义,两种套接口地址结构都定义了地址族类型成员,是自描述的结构。网络
另外为了使套接口函数可以统一处理全部协议族的套接口地址结构,定义了通用套接口地址:架构
/* 通用套接口地址结构 */ struct sockaddr { uint8_t sa_len; /* 结构体大小 */ sa_family_t fa_family; /* 地址族 */ char sa_data[14]; /* 协议地址 */ }
通用套接口地址结构的用处只有两个:一、用于套接口函数的声明,任何使用套接口地址指针为参数的函数,其指针所有声明为指向通用套接口地址类型。二、类型转换,用于将特定于协议族的地址指针转换为通用地址指针。dom
2.套接口函数socket
socket建立套接口描述符:tcp
#include <sys/socket.h> /* 功能:建立套接口描述符-sockfd。 参数:一、domain-协议族,经常使用AF_INET、AF_INET六、AF_UNIX或AF_LOCAL等。 二、type-套接口类型,经常使用SOCK_STREAM、SOCK_DGRAM 等。 三、protocol-协议,通常设置为0,协议有内核根据前两个参数选择。 返回值:成功则返回非负描述符,失败返回-1。 */ int socket(int domain, int type, int protocol)
bind将套接口描述符绑定到特定的套接口地址:函数
#include <sys/socket.h> #include <sys/socket.h> /* 功能:将套接口描述符-sockfd绑定到特定的套接口地址。 参数:一、sockfd-套接口描述符。 二、addr-指向通用套接口地址的指针。 三、addrlen-套接口地址结构的大小。 返回值:成功则返回0,失败返回-1,并修改errno为相应的值。 */ int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect将本地描述符链接到套接口地址addr指定的服务端:学习
#include <sys/socket.h> /* 功能:将本地描述符链接到套接口地址addr指定的服务端。 参数:一、sockfd-套接口描述符。 二、addr-指向通用套接口地址的指针。 三、addrlen-套接口地址结构的大小。 返回值:成功则返回0,失败返回-1,并修改errno为相应的值。 */ int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen将套接口描述符设置为被动链接状态,用于在指定端点进行监听:
#include <sys/socket.h> /* 功能:将套接口描述符设置为被动等待链接状态,用于在指定端点进行监听。 参数:一、sockfd-套接口描述符。 二、backlog-未完成链接对了和已完成链接队列的最大值。 返回值:成功则返回0,失败返回-1,并修改errno为相应的值。 */ int listen(int sockfd, int backlog);
accept用于服务器端接受客户端的一个链接:
#include <sys/socket.h> /* 功能:用于服务器端接受客户端的一个链接。 参数:一、sockfd-套接口描述符。 二、addr-指向通用套接口地址的指针,函数返回时保存了客户端套接口地址信息。 三、addrlen-套接口地址结构的大小。 返回值:成功则返回一个已链接到客户端的套接口描述符,失败返回-1,并修改errno为相应的值。 */ int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
3.字节序转换函数和地址转换函数
#include <arpa/inet.h> /* 功能:主机序转换成网络序。 参数:一、hostlong/hostshort-待转换32位长整形或者16位短整形。 返回值:转换成网络序的32位长整形或16为短整形。 */ uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort);
/* 功能:网络序转换成主机序。 参数:一、netlong/netshort-待转换32位长整形或者16位短整形。 返回值:转换成主机序的32位长整形或16为短整形。 */ uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
备注:多字节类型数据的表示/存储方式分为小端序和大端序。小端序,高位字节存储在高地址内存空间,低位字节存储在低位内存空间,大端序偏偏相反。对于不一样架构的主机序不同,有的架构主机序采用小端字节序,有的架构主机序采用大端架构,另外网络序采用大端字节序。当在网络上传输字节流时,不须要考虑字节序的问题,当在网络上传输多字节类型数据时须要考虑字节序问题。
验证本地host字节序的方法:
#include <stdlib.h> #include <stdio.h> int main(void) { union { short a; char c[sizeof(short)]; } t; t.a = 0x0102; /* 高位字节存存储于低地址内存, * 低位字节序存储于高地址内存 */ if (1 == t.c[0] && 2 == t.c[1]) printf("big-endian\n"); else printf("little-endian\n"); return 0; }
4.多字节操纵函数
string.h中定义了两组多字节类型操做函数,这两组函数不对待处理的多字节类型数据作任何假设。第一组函数由b开头,起源于4.2BSD,当前几乎全部支持套接口的系统都提供这一组函数。
/* 功能:将s指向大小为n的内存空间初始化为全0。 参数:一、s-内存地址。 二、n-内存大小 返回值:无。 */ void bzero(void *s, size_t n); /* 功能:内存拷贝。 参数:一、src-源内存地址。 二、dest-目标内存地址。 三、n-拷贝内存大小。 返回值:无。 */ void bcopy(const void *src, void *dest, size_t n); /* 功能:内存比较。 参数:一、s1-内存地址1。 二、s2-内存地址2。 三、n-比较的内存大小。 返回值:0-表示两块内存数据相同,非0-表示两块内存数据不一样。 */ int bcmp(const void *s1, const void *s2, size_t n);
第二组函数由mem开头,起源于ansi c,由任何支持ansi c标准的系统提供。
#include <strings.h> /* 功能:内存初始化。 参数:一、str-内存地址。 二、c-初始化值。 三、n-内存大小。 返回值:0-表示两块内存数据相同,非0-表示两块内存数据不一样。 */ void *memset(void *str, int c, size_t n) /* 功能:内存拷贝。 参数:一、src-源内存地址。 二、dest-目标内存地址。 三、n-拷贝内存大小。 返回值:无。 */ void *memcpy(void *dest, const void *src, size_t n); /* 功能:内存比较。 参数:一、s1-内存地址1。 二、s2-内存地址2。 三、n-比较的内存大小。 返回值:0-表示两块内存数据相同,非0-表示两块内存数据不一样。 */ int memcmp(const void *s1, const void *s2, size_t n);
除了上面的两组函数觉得,string.h头文件定义了专门处理字符串的函数,这些函数以str开头好比strcpy/strcmp等,这些函数假设处理的字符串都以0结尾。
5.套接口ip地址到字符串转换函数
#include <arpa/inet.h> /* 功能:将字符串IP地址转换成机器IP地址,。 参数:一、af-地址族:AF_INET, AF_INET6。 二、strptr-字符串ip。 三、addrptr-地址结构。 返回值:1-成功,0-针对af, strptr不是有效的ip地址格式,。 */ int inet_pton(int af, const char *strptr, void *addrptr); /* 功能:将机器IP地址转换成字符串IP地址,。 参数:一、af-地址族:AF_INET, AF_INET6。 二、addrptr-机器地址结构。 三、strptr-字符串ip。 四、size-strptr的长度,为了防止内核写溢出。 返回值:成功则返回指向strptr的指针,失败-返回null,并设置errno为相应值。。 */ const char *inet_ntop(int af, const void *addrptr, char *strptr, socklen_t size);
6. 套接口读写函数
readn从一个socket中读取n个字节(引用unp示例代码):
size_t Readn(int fd, void *vptr, size_t n) { size_t nleft; size_t nread; char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nread = read(fd, ptr, nleft)) < 0) { if (errno == EINTR) nread = 0; /* 系统调用被信号打断,从新调用 */ else return(-1); /* 发生异常 */ } else if (nread == 0) break; /* 读到结束符,读到本地tcp收到的fin时 */ nleft -= nread; ptr += nread; } return(n - nleft); /* return >= 0 */ }
Writen向套接口描述符中写入n个字节(引用unp示例代码):
/* 功能:从套接口中读取n个字节 返回值:n-成功;-1:失败*/ size_t Writen(int fd, const void *vptr, size_t n) { size_t nleft; size_t nwritten; const char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nwritten = write(fd, ptr, nleft)) <= 0) { if (nwritten < 0 && errno == EINTR) nwritten = 0; /* 被信号打断,再次调用 */ else return(-1); /* 写异常:write返回0-写一个tcp链接关闭的sock */ } nleft -= nwritten; ptr += nwritten; } return(n); }
Readline从描述符fd中读取一行(引用unp示例代码):
static int read_cnt;/* 缓冲区中可读的字节数 */ static char *read_ptr;/* 当前缓冲区中可读字节的指针 */ static char read_buf[MAXLINE];/* 读缓冲区 */ /* 功能:读去一个字节, 返回值:1-成功;0-读到文件结束符;-1-系统调用错误*/ static ssize_t my_read(int fd, char *ptr) { if (read_cnt <= 0) { /* 当缓冲区中可读字节数为0时,从文件中读数据到缓冲区 */ again: if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) { if (errno == EINTR) /* 系统调用被信号打断 */ goto again; return(-1); } else if (read_cnt == 0) /* 读到文件结束符,正常返回0 */ return(0); read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; /* 从缓冲区中读一个字节给用户 */ return(1); } /* 功能:从fd中读取一行到vptr中,行的最大长度maxlen 返回值:n-成功,实际读取的字节数;-1:读失败*/ ssize_t readline(int fd, void *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1; n < maxlen; n++) { if ( (rc = my_read(fd, &c)) == 1) { *ptr++ = c; if (c == '\n') break; /* newline is stored, like fgets() */ } else if (rc == 0) { *ptr = 0; return(n - 1); /* EOF, n - 1 bytes were read */ } else return(-1); /* error, errno set by read() */ } *ptr = 0; /* null terminate like fgets() */ return(n); } /* 功能:从fd中读取一行到vptr中,行的最大长度maxlen 返回值:n-成功;-1-失败,并打印错误日志*/ ssize_t Readline(int fd, void *ptr, size_t maxlen) { ssize_t n; if ( (n = readline(fd, ptr, maxlen)) < 0) err_sys("readline error"); return(n); }
Readline存在的问题:由于间接使用了静态全局变量,所以Readline是不可重入的,不是线程安全的。