第5章 Linux网络编程基础
5.1 socket地址与API
一.理解字节序
主机字节序一般为小端字节序.网络字节序一般为大端字节序.当格式化的数据在两台使用了不同字节序的主机之间直接传递时,接收端要根据自身采用的字节序决定是否对其接收到的数据进行转换.
在Linux上提供了4个函数来完成主机字节序和网络字节序之间的转换.
#include<netinet/in.h>
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);
htol表示"host to network long". 通常长整型用来转换IP地址,短整型用来转换端口号.但是任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序.
二.socket地址
socket地址有通用socket地址和专用socket地址.为了适应不同地址协议的数据地址格式,地址协议有以下几种:
地址族 | 描述 |
---|---|
AF_UNIX | UNIX本地域协议族 |
AF_INET | TCP/IPv4协议族 |
AF_INET6 | TCP/IPv6协议族 |
在这几种不同的地址协议中,需要不同的的结构体以便能存下其地址值.
-
通用socket地址:
/* Structure describing a generic socket address. /
struct sockaddr
{
_SOCKADDR_COMMON (sa); / Common data: address family and length. /
char sa_data[14]; / Address data. */
};
由于14字节的sa_data根本不能完全容纳多数协议族的地址值,因此.Linux定义了下面这个新的通用socket地址结构体.
struct sockaddr_storage
{
__SOCKADDR_COMMON (ss_); /* Address family, etc. */
__ss_aligntype __ss_align; /* Force desired alignment. */
char __ss_padding[_SS_PADSIZE];
};
-
专用socket结构体
//AF_UNIX.
/* Structure describing the address of an AF_LOCAL (aka AF_UNIX) socket. */
struct sockaddr_un
{
__SOCKADDR_COMMON (sun_);
char sun_path[108]; /* Path name. */
};
//用于IPv4
//struct sockaddr_in
{
short int sin_family; // 地址族:AF_INET
unsigned short int sin_port; // 端口号要用网络字节序表示
struct in_addr sin_addr;
};
sturct in_addr
{
u_int32_t s_addr;// 要用网络字节序表示
};
// 当然还有适用于IPv6的,就不列举出来了.
主要是通用的socket不太好用,所以Linux就为各个协议族提供了专门的socket地址结构体.
注意:但是所有的专用socket地址在实际使用时都需要转化为通用的socket地址类型(通过强制转换就行了),这是因为所有的socket编程接口使用的地址参数的类型都是sockaddr.
- IP地址转换函数
由于人们习惯了使用可读性好的字符串来表示IP地址,但是在编程中都是使用的整数,所以需要在网络字节序与字符串字符形式之间转换.
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
unsigned long int inet_addr(const char *cp);
int inet_aton(const char * cp,struct in_addr *inp);
char * inet_ntoa(struct in_addr in);
第1,2个都是将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址.
第3个不具备可重入性(内部有一个全局静态变量).
推荐使用下面具有相同功能的函数进行转换,并且它们同样适用于IPv4地址和IPv6地址.
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
af参数指定地址族,cnt指定目标存储单元,可使用以下宏进行替换.
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
网络字节序,主机字节序和IP字符串之间的关系我总结如下图:
① | ② | ③ |
---|---|---|
网络字节序 | 主机字节序 | IP字符串 |
① -> ②:ntohl,ntohs ; ② -> ①:htons,htonl.
① -> ③:inet_ntoa,inet_ntop ; ③ -> ①:inet_aton,inet_pton.
5.2 创建socket & 5.3 命名socket & 5.4 监听socket & 5.5 接受连接
下面将通过实例来讲解在服务端创建一个socket连接的全过程:
// 1.创建socket
//int socket(int domain,int type,int protocol);
domain | type | protocol |
---|---|---|
PF_UNIX:本地域协议族. PF_INET:对于TCP/IP协议来说,用于IPv4. PF_INET6:用于IPv6. |
SOCK_STREAM:表示传输层使用的是TCP协议. SOCK_DGRAM:表示传输层使用的是UDP协议. SOCK_NONBLOCK:新创建的socket设为非阻塞的. SOCK_CLOEXEC:用fork调用创建子进程时在子进程中关闭该socket. |
protocol:在前两个参数构成的协议集合下,再选择一个具体的协议. 通常前两个已经决定了,所以可以设置为0. |
// 成功返回一个socket文件描述符,失败返回-1并设置error.
int sock = socket(PF_INET,SOCK_STREAM,0);
// 2.命名socket
// 将一个socket与socket地址绑定称为给socket命名.
// 通常服务端才需要命名socket,而客户端采用匿名方式,即使用操作系统自动分配的socket地址.
// int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
// 成功返回0,失败返回-1并设置error.
// 常见错误为: EACCESS:被绑定的地址是受保护的地址,如绑定到了知名服务端口上.
// EADDRINUSE:被绑定的地址正在被使用,如处于TIME_WAIT的socket地址.
sockfd | my_addr | addrlen |
---|---|---|
未命名的socket文件描述符 | socket分配的地址 | 该socket地址的长度 |
// 创建IPv4 socket地址,专用的socket地址结构,最终需要强制转换.
struct sockaddr_in address;
// 设置地址族,IPv4.
address.sin_family = AF_INET;
// 设置ip地址 将字符转换为网络字节序,如下写也行,但是不具备重入性,故使用第二种.
// address.sin_addr.s_addr = inet_ntoa("192.168.1.108");
inet_pton(AF_INET,ip,&address.sin_addr);
// 设置端口号,将端口号转为网络字节序.
address.sin_port = htons(port);
int ret = bind(sock,(struct sockaddr *) &address,sizeof(address));
// 3. 监听socket
// 创建一个监听队列存放待处理的客户连接.
// int listen(int sockfd,int backlog);
// 自linux2.2之前连接状态是指SYN_RCVD和ESTABLISHED,之后指的是ESTABLISHED.
// 成功返回0,失败返回-1并设置error.
sockfd | backlog |
---|---|
指定被监听的socket | 指定内核监听队列的最大长度.若大于此长度,将不再受理. |
ret = listen(sock,backlog);
// 4.接受连接
// 从listen监听队列中接受一个连接.
// int accept(int sockfd,struct sockaddr * addr,int * addrlen);
// accept只是从监听队列中取出连接,而无论连接处于何种状态,更不关心任何网络状况的变化.
// 成功返回被接受的连接标识,失败返回-1并设置error.
sockfd | addr | addrlen |
---|---|---|
执行过listen调用的监听socket. | 用来获取被接受连接的源端socket地址. | 该addr地址的长度. |
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock,(struct sockaddr *)&client,&client_addrlength);
5.6 发起连接
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
// sockfd: 有socket系统调用返回的一个socket.
// serv_addr: 服务器监听的socket地址.
// addrlen: 该serv_addr地址的长度.
// 常见错误为: ECONNREFUSED 连线要求被server端拒绝.
// ETIMEDOUT 企图连线的操作超过限定时间仍未有响应.
// 成功返回成功连接的socket标识符,失败返回-1并设置error.
connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address);
5.7 关闭连接
关闭连接可以使用以下两种方式关闭:
函数 | 描述 |
---|---|
int close(int fd) | 使用引用计数,当fd的引用计数为0时,才真正关闭连接. |
int shutdown(int sockfd,int how) | howto决定了shutdown的行为. SHUT_RD:关闭读. SHUT_WR:关闭写. SHUT_RDWR:关闭读写. 成功返回0,失败返回-1并设置error. |
5.8 数据读写
一. TCP数据读写
用于TCP流数据读写的系统调用是:
#include<sys/types.h>
#include<sys/socket.h>
int recv(int s,void *buf,int len,unsigned int flags);
int send(int s,const void * msg,int len,unsigned int falgs);
- recv可能返回0,这表示对方已经关闭了连接.
- flags对数据收发提供了额外的控制.
发送带外数据的例子:
发送带外数据: 注意flags为MSG_OOB.
...
const char *oob_data = "abc";
const char *normal_data = "123";
send(sockfd, normal_data, strlen(normal_data), 0);
send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
send(sockfd, normal_data, strlen(normal_data), 0);
...
接收带外数据:
...
char buffer[ BUF_SIZE];
memset(buffer, '\0', BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE -1, 0);
printf("got %d bytes of normal data '%s'\n", ret, buffer);
memset(buffer, '\0', BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE -1, MSG_OOB);
printf("got %d bytes of oob data '%s'\n", ret, buffer);
memset(buffer, '\0', BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE -1, 0);
printf("got %d bytes of normal data '%s'\n", ret, buffer);
...
接收到的数据结果为:
got 5 bytes of normal data '123ab'
got 1 bytes of normal data 'c'
got 3 bytes of normal data '123'
可见'c'被当作做了带外数据接收,而且服务器对正常数据的接收也被带外数据截断.正像第3章讨论的那样.
flags 参数只对send和recv当前调用生效,可以通过setsockopt(这一章后面会讲到)永久修改socket的某些属性.
二.UDP数据读写
socket编程接口用于UCP数据报读写的系统调用是:
#include<sys/types.h>
#include<sys/socket.h>
int recvfrom(int s,void *buf,int len,unsigned int flags ,struct sockaddr *from ,int *fromlen);
int sendto ( int s , const void * msg, int len, unsigned int flags, const struct sockaddr * to , int tolen );
它也可以用于面向连接的socket数据读写,只需要把最后两个参数设置为NULL,以忽略发送端/接收端的socket地址.因为本来就已经知道了.
三.通用数据读写函数
#include<sys/types.h>
#include<sys/socket.h>
int recvmsg(int s,struct msghdr *msg,unsigned int flags);
int recvfrom(int s,void *buf,int len,unsigned int flags ,struct sockaddr *from ,int *fromlen);
可见对于数据的读写,有TCP专用的也有UDP专用的,甚至还有通用的.
5.9 带外标记
内核通知应用程序带外数据到达的两种常见方式为:
- I/O 复用产生的异常事件.
- SIGURG信号.
当应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的位置.判断带外数据的到达,可以使用以下函数:
#include <sys/socket.h>
int sockatmark(int s);
5.10 地址信息函数
用于获取本端/远端 socket地址:
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t * addrlen);
int getpeername(int sockfd, struct sockaddr *addr, socklen_t * addrlen);
5.11 socket选项
#include <sys/socket.h>
int getsockopt(int s,int level,int optname,void* optval,socklen_t* optlen);
int setsockopt(int s,int level,int optname,const void * optval,socklen_toptlen);
用于读取/设置socket文件描述符属性.
level: 指定要操作那个协议的选项.
option_name & option_len: 被操作选项的值和长度.
- 和fcntl用于控制文件描述符属性的通用POSIX方法类似.
- 对服务器而言,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效,而accept从listen监听队列中接收的连接至少完成了TCP三次握手的前两个阶段.
下面讨论部分重要的socket选项.
一.SO_REUSEADDR 选项
即使sock处于TIME_WAIT,与之绑定的socket选项也可以立即被重用,也可以修改内核参数"/proc/sys/net/ipv4/tcp_tw_recycle"来快速回收被关闭的socket,如此一来TCP连接根本就不进入TIME_WAIT状态,进而允许应用程序立即重用本地的socket地址.
int reuse=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDE,&reuse,sizeof(reuse));
二.SO_RCVBUF 和 SO_SNDBUF 选项
分别设置TCP接收/发送缓冲区的大小.
可以直接修改 "/proc/sys/net/ipv4/tcp_rmem" 和 "/proc/sys/net/ipv4/tcp_wmem" 强制TCP接收缓冲区个发送缓冲区大小没有最小限制.
TCP接收缓冲区最小为256字节,发送缓冲区最小为2048字节.(系统不同会有差异)
当使用setsockopt设置接收缓冲区大小时,大小会加倍,并且不得小于某个最小值.
若设置太小,会涉及到通告窗口大小的调整.
下面是使用setsockopt修改的例子:
...
int len = sizeof(sendbuf);
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof(sendbuf));
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, (socklen_t *)&len);
printf("the tcp send buffer size after setting is %d\n", sendbuf);
...
三.SO_RCVLOWAT 和 SO_SNDLOWAT 选项
分别表示TCP接收缓冲区和发送缓冲区的低水位标记,一般用来判断socket是否可读可写.
表现为当接收缓冲区可读数据大于其低水位标记,I/O复用系统将通知应用程序从对应的socket读取数据.
当发送缓冲区的空闲空间大于其低水位标记时,I/O复用系统将通知应用程序往对应的socket写入数据.
- 默认TCP接收缓冲区和发送缓冲区的低水位标记为1字节.
四.SO_LINGER 选项
控制close系统在关闭TCP连接时的行为.
在设置(获取)SO_LINGER选项的时候,我们需要给setsockopt(getsockopt)系统调用传递一个linger类型的结构体.
struct linger {
int l_onoff;
int l_linger;
};
它的值提供了三种关闭方式:
l_onoff == 0 :
close()立刻返回,底层会将未发送完的数据发送完成后再释放资源,即优雅退出.l_onoff != 0; l_linger == 0 :
close立即关闭,丢弃发送缓冲区的残留数据,发送复位报文段.l_onoff != 0; l_linger > 0 :
1)阻塞:发送发送缓冲区内残留数据,close等待长度为l_linger的时间,直到对方确认.
2)非阻塞:close立即返回,可根据返回值看残留数据是否发送完成.
5.12 网络信息API
一.gethostbyname 和 gethostbyaddr
分别是根据主机名称获取主机的完整信息和根据ip地址获取主机的完成信息.gethostbyname 通常先在本地"/etc/hosts"配置文件中查找主机,若没有找到,再去访问DNS服务器.
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
而hostent结构体类型为:
struct hostent {
char *h_name; // 主机名
char **h_aliases; // 主机别名列表
int h_addrtype; // 地址类型
int h_length; // 地址长度
char **h_addr_list; // 按网络字节序列列出的主机IP地址列表
};
二.getservbyname 和 getservbyport
分别是根据名称获取某个服务的完整信息和根据端口号获取某个服务的完整信息,他们实际上都是通过读取"/etc/services"文件来获取服务的信息的.
#include <netdb.h>
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
而servent的结构为:
struct servent {
char *s_name; /* 服务名称 */
char **s_aliases; /* 服务的别名列表 */
int s_port; /* 端口号 */
char *s_proto; /* 服务类型,通常是tcp或udp */
}
使用例子:
...
const char *host = argv[1];
/*获取目标主机地址信息*/
struct hostent *hostinfo = gethostbyname(host);
assert(hostinfo);
/*获取daytime服务信息*/
struct servent *servinfo = getservbyname("daytime", "tcp");
assert(servinfo);
fprintf(stdout, "daytime port is %d\n", ntohs(servinfo->s_port));
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
/*注意下面的代码, 因为h_addr_list本身是使用网络字节的地址列表,
* 所以使用其中的IP地址时, 无须对目标IP地址转换字节序*/
address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;
...
三.getaddrinfo
既能通过主机名获取IP地址,也能通过服务名获取端口号.内部调用的是gethostbyname 和 getservbyname.
getaddrinfo会隐含得分配内存,所以,调用完getaddrinfo后要使用freeaddrinfo来配对的释放内存.
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res);
void freeaddrinfo(struct addrinfo *res);
四.getnameinfo
通过socket地址同时获得以字符串表示的主机名和服务名.内部调用的是gethostbyaddr 和 getservbyport.
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,
char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flags);
关于第五章的总结
了解了Linux网络编程基础API与内核TCP/IP协议族.
通过API了解了建立连接之
服务端上的"创建(socket)-绑定(bind)-监听(listen)-接受连接(accept)"
客户端的"创建(scoket)-请求连接(connect)"还有可以通过setsockopt(getsockopt)来设置和获取关于socket连接的状况.进而使其满足某些特殊要求.
了解了一些函数用于获取主机的相关信息.
From
Aaron-z/linux-server-high-performance
2017/2/3 18:48:07