Chinaunix首页 | 论坛 | 博客

OS

  • 博客访问: 2280754
  • 博文数量: 691
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 2660
  • 用 户 组: 普通用户
  • 注册时间: 2014-04-05 12:49
个人简介

不浮躁

文章分类

全部博文(691)

文章存档

2019年(1)

2017年(12)

2016年(99)

2015年(207)

2014年(372)

分类: 网络与安全

2014-11-21 21:31:24

5  Linux网络编程基础API


1、socket地址API。
2、socket基础API。主要定义在sys/socket.h头文件中,包括创建socket、命名socket、监听socket、接收连接、发起连接、读写数据、获取地址信息、检测带外标记,以及读取和设置socket选项。
3、网络信息API。Linux提供了一套网络信息API,实现主机名和ip地址之间的转换,以及服务名称和端口号之间的转换。这些api都定义在netdb.h头文件中。




主机字节序和网络字节序
大端模式和小端模式。
问题:编程判断大小端模式?


4个函数来完成主机字节序和网络字节序之间的转换:
#include
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
这4个函数,长整型函数通常用来转换ip地址,短整型函数用来转换端口号,当然不限于此。




通用socket地址:
#include
struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
};


sa_family类型如下:
协议簇   地址簇
PF_UNIX     AF_UNIX
PF_INET     AF_INET
PF_INET6    AF_INET6
sa_data用于存放socket地址值:
协议簇   地址含义和长度
PF_UNIX     文件的路径名,长度可达108字节
PF_INET   16bit端口号和32bit ipv4地址,共6字节
PF_INET6  ...
14字节的sa_data根本无法完全容纳多数协议簇的地址值。因此Linux定义了下面这个新的通用的socket地址结构体:
#include
struct sockaddr_storage{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[128-sizeof(__sa_align)];
};
这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)。


专用socket地址:
#include
struct sockaddr_un{
sa_famlily_t sin_family;//地址簇:AF_UNIX
char sun_path[108];//文件路径名
};


struct sockaddr_in{
sa_family_t sin_family;
u_int16_t sin_port;
struct in_addr sin_addr;
};
struct in_addr{
u_int32_t s_addr;
};


struct sockaddr_in6{
sa_family_t sin6_family;
u_int16_t sin6_port;
u_int32_t sin6_flowinfo;
struct in6_addr sin6_addr;
u_int32_t sin6_scope_id;
};
struct in6_addr{
unsigned char sa_addr[16];
};


所有专用的socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用的socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。




ip地址转换函数:
#include
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp, struct in_addr* inp);
char* inet_ntoa(struct in_addr in);//内部使用静态变量,不可重入
inet_addr函数将用点分十进制字符串表示的ipv4地址转化为用网络字节序整数表示的ipv4地址。失败时返回INADDR_NONE。


#include
int inet_pton(int af, const char* src, void* dst);
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);
cnt参数指定目标存储单元的大小。下面的两个宏能帮助我们指定这个大小:
#include
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46




#include
#include
int socket(int domain, int type, int protocol);


int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
bind成功返回0,失败返回-1并设置errno。其中两中常见的errno是EACCES和EADDRINUSE,含义分别是:
EACCES:被绑定的地址是受保护的地址,仅超级用户能访问。比如普通用户将socket绑定到知名服务端口(端口号为0-1023)。
EADDRINUSE:被绑定的地址正在使用。如将socket绑定到一个处于TIME_WAIT状态的socket地址。


int listen(int sockfd, int backlog);
指示内核监听队列的最大长度。监听队列长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。
监听队列中完整连接的上线通常比backlog值略大。


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


int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
出错返回时两种常见的errno是ECONNREFUSED(目标端口不存在,连接被拒绝)ETIMEDOUT(连接超时)


int close(fd);
close系统调用并非总是关闭一个连接,而是将fd的引用计数减1.只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
如果无论如何都要立即终止连接(而不是将socket引用计数减1),可以使用shutdown系统调用(专为网络编程设计的)
#include
int shutdonw(int sockfd, int howto);
howto参数:
SHUT_RD//关闭sockfd上读的这一半。该socket接收缓冲区中的数据都被丢弃
SHUT_WR//关闭sockfd上写的这一半。发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该socket文件描述符执行写操作。连接处于半关闭状态。
SHUT_RDWR//同时关闭读写。
由此可见,shutdown能够分别关闭socket上的读或写,或者都关闭。而close在关闭连接时只能将socket上的读和写同时关闭。




数据读写:
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);
recv成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次调用recv,才能读取到完整的数据。recv可能返回0,这意味着通信对方已经关闭连接了。
flags参数:
MSG_CONFIRM//...
MSG_DONTROUTE//不查看路由表,直接将数据发送到本地局域网内的主机
MSG_DONTWAIT//对socket的此次操作将是非阻塞的
MSG_MORE//告诉内核应用程序还有更多数据要发送,内核将超时等待新数据写入tcp发送缓冲区后一并发送。这样可以防止tcp发送过多小的报文段,从而提高传输效率
MSG_WAITALL//读操作仅在读取到指定数量的字节后才返回
MSG_PEEK//窥探读缓存中的数据,此次读操作不会导致这些数据被清除
MSG_OOB//发送或接收紧急数据
MSG_NOSIGNAL//往读端关闭的管道或者socket连接中写数据时不引发SIGPIPE信号


服务器对正常数据的接收会被带外数据截断,即前一部分正常数据和后续正常数据是不能被一个recv调用全部读出的。




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, void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);




通用数据读写函数:
#include
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
struct msghdr{
void* msg_name;//socket地址
socklen_t msg_namelen;
struct iovec* msg_iov;
int msg_iovlen;
void* msg_control;//指向辅助数据的起始位置
socklen_t msg_controllen;
int msg_flags;//复制函数中flsgs参数,并在调用过程中更新
};
struct iovec{
void* iov_base;//内存起始地址
size_t iov_len;//这块内存的长度
};




带外标记:
Linux内核检测到tcp紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式:IO复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。
#include
int sockatmark(int sockfd);
sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是,sockatmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。




地址信息函数:
#include
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);




socket选项:
#include
int getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len);


选项列表:
SOL_SOCKET: SO_REUSEADDR\SO_TYPE\SO_DONTROUTE\SO_RCVBUF\SO_SNDBUF\SO_KEEPALIVE\SO_OOBINLIEN\SO_LINGER\SO_RCVLOWAT\SO_SNDLOWAT\SO_RCVTIMEO\SO_SNDTIMEO;


IPPROTO_IP:IP_TOS\IP_TTL


IPPROTO_IPV6:IPV6_NEXTHOP\...


IPPROTO_TCP:TCP_MAXSEG\TCP_NODELAY


对服务器而言,有部分socket选项只能在调用listen前针对监听socket设置才有效。


SO_REUSEADDR选项:可以设置该选项来强制使用被处于TIME_WAIT状态的连接占用的socket地址。


SO_RCVBUF SO_SNDBUF选项:当我们用setsockopt来设置tcp的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。tcp接收缓冲区的最小值是256字节,而发送缓冲区的最小值是2048字节(不同系统可能不同)。这样做的目的是确保一个tcp连接拥有足够的空闲缓冲区来处理拥塞(比如快速重传算法就期望tcp接收缓冲区能至少容纳4个大小为SMSS的tcp报文段)。


tcpdump输出中的wscale窗口扩大因子m,则窗口为当前值 × 2的m次方。


SO_LINGER选项:
用于控制close系统调用在关闭tcp连接时的行为。默认情况下,当使用close调用来关闭一个socket时,close将立即返回,tcp模块负责把该socket对应的tcp发送缓冲区中残留的数据发送给对方。使用时需要传递一个linger类型的结构体:
#include
struct linger{
int l_onoff;//开启(非0)还是关闭(0)
int l_linger;//滞留时间
};


l_onoff不为0,l_linger=0。此时close调用立即返回,tcp模块将丢弃被关闭的socket对应的tcp发送缓冲区中残留的数据,同时给发送方一个复位报文段。因此,这种情况给服务器提供了异常终止一个连接的方法。
l_onoff不为0,l_linger>0。




网络信息API:
#include
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
struct hostent{
char* h_name;
char** h_aliases;
int h_addrtype;
int h_length;
char** h_addr_list;
};


通过读取/etc/services文件来获取服务的信息。
#include
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
struct servent{
char* s_name;
char** s_aliases;
int s_port;
char* s_proto;
};
以上4个函数都是不可重入的,即非线程安全的。不过netdb.h头文件给出了它们的可重入版本(在原函数名尾部加上_r)。




#include
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
struct addrinfo{
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
char* ai_canonname;
struct sockaddr* ai_addr;
struct addrinfo* ai_next;
};
result指针原本是没有指向一块合法内存的,所以,getaddrinfo调用结束后,我们必须使用如下配对函数来释放这块内存:
#include
void freeaddrinfo(struct addrinfo* res);




getnameinfo函数通过socket地址同时获得以字符串表示的主机名和服务名。它是否可重入取决于其内部调用的gethostbyaddr和getservbyport函数是否是它们的可重入版本。
#include
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);


Linux下strerror能将数值错误码errno转换成易读的字符串形式。同样,下面的函数可将错误码转换成其字符串形式:
#include
const char* gai_strerror(int error);

阅读(996) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~