一、套接字编程简介
1.1 套接字地址结构
大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。每个协议族都定义它自己的套接字地址结构。这些结构的名字均以**sockaddr_**开头,并以对应每个协议族的唯一后缀结尾。
- IPv4套接字地址结构
以sockaddr_in命名,定义在netinet/in.h中:
struct sockaddr_in{
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct in_addr{
in_addr_t s_addr;
};
注: (1)ip地址和端口号在套接字地址中总是以网络字节序来存储的 (2)32位ip地址存在两种不同的访问方法。sin_addr按in_addr结构体引用或sin_addr.s_addr按in_addr_t引用
- 通用套接字地址结构
作为函数参数的套接字结构体指针,必须处理来自所支持的任何协议族的套接字地址结构,所以在如何声明所传递指针的数据类型上存在问题。(有了ANSI C后很简单,使用void*即可;然而套接字函数是在之前定义的)。采取的办法是在sys/socket.h头文件中定义一个通用的套接字地址结构:
struct sockaddr{
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};
- IPv6套接字地址结构
struct sockaddr_in6{
uint8_t sin6_len;
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
struct in6_addr{
uint8_t s6_addr[16];
};
1.2 字节排序函数
大端(big-endian):高字节存储在起始位置 小段(little-endian):低字节存储在起始位置
用联合体(关键代码)判断系统的主机字节序:
union {
short s;
char c[sizeof(short)];
}un;
un.s = 0x0102;
if(sizeof(short) == 2){
if(un.c[0] == 1 && un.c[1] == 2)
else if(un.c[0] == 2 && un.c[1] == 1)
else
}
两种字节序之间的转换:
#include <netinet/in.h>
uint16_t htons(uint16_t h16);
uint16_t htonl(uint32_t h32);
uint16_t ntohs(uint16_t n16);
uint16_t ntohs(uint16_t n32);
1.3 字节操纵函数
名字以b开头(表示字节)的一组函数起源于4.2BSD,以mem开头(表示内存)的一组函数起源于ANSI C标准。
#include <strings.h>
void bzero(void* dest, size_t nbytes);
void bcopy(const void* src, void* dest, size_t nbytes);
int bcmp(const void* ptr1, const void* ptr2, size_t nbytes);
#include <string.h>
void* memset(void* dest, int c, size_t len);
void* memcpy(void* dest, void* src, size_t nbytes);
int memcmp(const void* ptr1, const void* ptr2, size_t nbytes);
不同之处: 所有的memXXX函数都需要一个长度参数,而且总是最后一个 (1)memset将指定数目的字节置为c,而bzero置为0 (2)memcpy与bcopy中指针参数顺序是相反的,记忆:dest=src,即赋值顺序 (3)memcpy比较字符串,非0时,返回值取决于第一个不等的字节。*ptr1>*ptr2(基于unsigned char比较),则大于0,否则小于0.
1.4 地址转换函数
两组转换函数: (1)inet_aton、inet_addr(已舍弃)和inet_ntoa在点分十进制数串与它长度为32位的网络字节序二进制之间转换IPv4地址 (2)较新的inet_pton和inet_ntop对于IPv4地址和IPv6地址都适用。
#include <arpa/inet.h>
int inet_aton(const char* strptr, struct in_addr* addrptr);
char* inet_ntoa(struct in_addr inaddr);
函数名中的p和n分别代表表达(presentation)和数值(numeric)。
#include <arpa/inet.h>
int inet_pton(int family, const char* strptr, void* addrptr);
const char* inet_ntop(int family, const void* addrptr, char* strptr, size_t len);
这两个函数的family函数即可以是AF_INET,也可以是AF_INET6。 inet_ntop将addrptr转换为strptr, len参数时目标存储单元的大小,以免该函数溢出其调用者的缓冲区。
1.5 自定义的函数readn、writen和readline
字节流套接字上的read和write函数所表现的行为不同于通常的文件I/O。字节流套接字上调用read和write输入或输出的字节数可能比请求的数量少,然而这不是出错的状态。通常是因为内核中套接字的缓冲区满,此时所需的是调用者再次调用read或write函数,以输入或输出剩余的字节。
为了避免让调用者来处理不足的字节计数值,我们使用xxxn函数:
#include "unp.h"
ssize_t readn(int fileds, void* buff, size_t nbytes);
ssize_t writen(int fileds, const void* buff, size_t nbytes);
ssize_t readline(int fileds, void* buff, size_t maxlen);
#include "unp.h"
ssize_t readn(int fd, void* vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char* 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;
nleft -= nread;
ptr += nread;
}
return (n - nleft);
}
ssize_t writen(int fd, const void* vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char* ptr = vptr;
nleft = n;
while(nleft > 0){
if((nwritten = write(fd, ptr, nleft)) <= 0){
if(nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
readn和writen的情况有所不同,read时,对端可能随时关闭套接字而读到EOF,此时未读到n字节数据;而writen在不出错的情况下,一定会写完全部数据, 返回n。
|