Linux网络编程基础API
socket地址API
socket地址:一个IP地址和端口对(ip,port),唯一表示了使用TCP通信的一端
字节序
大小端字节序
32位机的CPU累计器一次能装载(至少)4字节,即一个整数。这4个字节在内存中的排列顺序将影响其被累加器装载成的整数的值
判断大小端字节序
联合判断
void byteorder(){
union {
short value;
char union_bytes[sizeof(short)];
}test;
test.value = 0x0102;
if((test.union_bytes[0]==1)&&(test.union_bytes[1]==2)){
printf("big endian\n");
}
else if((test.union_bytes[0]==2)&&(test.union_bytes[1]==1)){
printf("littel endian\n");
}
else{
printf("unknow...\n");
}
}
指针判断
void byteorder(){
int a=0x12345678;
char *p = NULL;
p = (char*)&a;
if(*p==0x78){
printf("littel endian\n");
}
else if(*p==0x12){
printf("big endian\n");
}
}
网络字节序与主机字节序
现代PC大多采用小端字节序,因此小端字节序被称为主机字节序
问题:在两台使用不同字节序的主机之间(或同一台机器的两个进程之间,一个用C编写,一个用java编写,java虚拟机采用大端字节序)直接传递时,接收端会错误解释
解决:发送端将发送的数据转化成大端字节序数据后再发送,接收端根据自身采用的字节序决定是否对接收到的数据进行转换
大端字节序被称为网络字节序,对所有接收数据的主机提供一个正确解释收到的格式化数据的保证
转换函数
#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);
通用socket地址
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
}
地址族类型通常与协议族类型(protocol family 或domain )对应
协议族就是不同协议的集合
地址族就是一个协议族所使用的地址集合
地址族和协议族其实是一样的,值也一样,都是用来识别不同协议的,为什么要搞两套东西呢?这是因为之前UNIX有两种风格系统:BSD系统和POSIX系统,对于BSD系统,一直用的是AF,对于POSIX系统,一直用的是PF。Linux作为后起之秀,为了兼容,所以两种都支持,这样两种风格的UNIX下的软件就可以在Linux上运行了
宏PF_* 与AF_* 均定义在bits/socket.h 头文件中
sa_date 存放socket地址值,不同协议族的地址值具有不同含义和长度
14字节的sa_data 无法容纳多数协议族的地址值,Linux定义了一个新的通用socket地址结构体
#include<bits/socket.h>
strcut sockaddr_storage{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[128-sizeof(__ss_align)];
}
专用socket地址
通用socket地址在设置与获取IP地址和端口号时需要执行烦琐的位操作,Linux为各个协议族提供专门的socket地址结构体
#include<sys/un.h>
struct sockaddr_un{
sa_family_t sin_family;
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 int6_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地址转换函数
人们习惯用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPv4地址,以及用十 六进制字符串表示IPv6地址。但编程中需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,要把整数表示的IP地址转化为可读的字符串
#include<arpa/inet.h>
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);
int inet_pton(int af,const char*src,void*dst);
const char*inet_ntop(int af,const void*src,char*dst,socklen_t cnt);
#include<netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
#include<arpa/inet.h>
#include<stdio.h>
int main(){
struct in_addr in1;
in1.s_addr=127;
struct in_addr in2;
in2.s_addr=128;
char* a = inet_ntoa(in1);
printf("%s\n",a);
char* b = inet_ntoa(in2);
printf("%s\n",a);
printf("%s\n",b);
return 0;
}
socket基础API
创建socket
socket为可读、可写、可控制、可关闭的文件描述符
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
流服务:发送端send()只是将数据写到TCP发送缓冲区中,然后将发送缓冲区中的数据打包成报文段发送出去。接收端又将接收到的报文段写到缓冲区中,最后recv()直接取数据。数据没有明确分割(由底层做分割),不分一定的报文段,什么时候想发便可将写入缓冲区的数据,进行打包再发送,即send()与recv()的次数没有必然联系
数据报:发送端sendto()将数据直接打包成相对应的报文段发送。数据有明确分割,拿数据按报文段拿
命名socket
协议、IP、port
创建socket时指定了地址族,但未指定使用该地址族中的哪一个具体socket地址,将socket与socket地址绑定称为给socket命名
在服务器程序中,通常要命名socket,因为只有命名后客户端才能知道该如何连接它。客户端则通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址
在一个pc上开启多个客户端进程, 如果指定固定端口, 必然会造成端口冲突, 影响通信。所以,客户端不指定端口,由操作系统自己分配。这样,实际上就是操作系统对客户端的socket进行了隐式的命名(名称中的端口是随机的)
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr*my_addr,socklen_t addrlen);
监听socket
socket被命名后还不能马上接受客户连接,需要使用系统调用创建一个监听队列以存放待处理的客户连接
#include<sys/socket.h>
int listen(int sockfd,int backlog);
编写一个程序,其backlog为5,测试得ESTABLISHED状态的链接有6个
多次修改backlog,最终完整链接的最多总是(backlog+1)个
在不同系统上,运行结果会有差别,但监听队列中完整连接的上限通常比backlog值略大
接受连接
从listen监听队列中接受一个连接
#include<sys/socket.h>
#include<sys/types.h>
int accept(int sockfd,struct sockaddr* addr,socklen_t *addrlen);
若监听队列中处于ESTABLISHED状态的连接对应的客户端出现网络异常,那么服务器对该连接执行的accept调用是否成功
accept只是从监听队列中取出连接,不管连接处于何种状态,也不关心网络状况的变化
发起连接
客户端主动与服务器建立连接
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd,const struct sockaddr& serv_addr,socklen_t addrlen);
关闭连接
#include<unistd.h>
int close(int fd);
#include<sys/socket.h>
int shutdown(int sockfd,int howto);
数据读写
TCP数据读写
#include<sys/types.h>
#include<sys/socket.h>
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);
flags 参数为数据收发提供了额外的控制
UDP数据读写
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recvfrom(int sockfd,void*buf,size_t len,int flags,struct sockaddr*src_addr,socklen_t*addrlen);
ssize_t sendto(int sockfd,const void*buf,size_t len,int flags,const struct sockaddr*dest_addr,socklen_t addrlen);
通用数据读写函数
不仅可用于TCP流数据,也可用于UDP数据报
#include<sys/socket.h>
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;
socklen_t msg_namelen;
struct iovec* msg_iov;
int msg_iovlen;
void*msg_control;
socklen_t msg_controllen;
int msg_flags;
};
struct iovec {
void*iov_base;
size_t iov_len;
};
带外标记
在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据
#include<sys/socket.h>
int sockatmark(int sockfd);
地址信息函数
#include<sys/socket.h>
int getsockname(int sockfd,struct sockaddr*address,socklen_t*address_len);
int getpeername(int sockfd,struct sockaddr*address,socklen_t*address_len);
socket选项
专门用来读取和设置socket文件描述符属性
#include<sys/socket.h>
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);
对服务器而言,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效。这是因为连接socket只能由accept调用返回,而accept从listen监听队列中接受的连接至少已经完成了 TCP三次握手的前两个步骤(因为listen监听队列中的连接至少已进入SYN_RCVD状态),这说明服务器已经往被接受连接上发送出了TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设置,比如TCP最大报文段选项(该选项只能由同步报文段来发送)。对这种情 况,Linux给开发人员提供的解决方案是:对监听socket设置这些socket选项,那么accept返回的连接socket将自动继承这些选项。这些socket选项包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、 TCP_MAXSEG和TCP_NODELAY。而对客户端而言,这些socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已完成
网络信息API
gethostbyname和gethostbyaddr
gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。gethostbyname函数通常先在本地的/etc/hosts配置文件中查找主机,如果没有找到,再去访问DNS服务器
#include<netdb.h>
struct hostent*gethostbyname(const char*name);
struct hostent*gethostbyaddr(const void*addr,size_t len,int type);
#include<netdb.h>
struct hostent {
char*h_name;
char**h_aliases;
int h_addrtype;
int h_length;
char**h_addr_list
};
getservbyname和getservbyport
通过读取/etc/services文件来获取服务的信息的
#include<netdb.h>
struct servent*getservbyname(const char*name,const char*proto);
struct servent*getservbyport(int port,const char*proto);
#include<netdb.h>
struct servent {
char*s_name;
char**s_aliases;
int s_port;
char*s_proto;
};
getaddrinfo
既能通过主机名获得IP地址(内部使用的gethostbyname函数),也能通过服务名获得 端口号(内部使用的是getservbyname函数)。它是否可重入取决于其内部调用的gethostbyname和 getservbyname函数是否是它们的可重入版本
#include<netdb.h>
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;
};
#include<netdb.h>
void freeaddrinfo(struct addrinfo*res);
getnameinfo
能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数) 和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的gethostbyaddr和 getservbyport函数是否是它们的可重入版本
#include<netdb.h>
int getnameinfo(const struct sockaddr*sockaddr,socklen_t addrlen,char*host,socklen_t hostlen,char*serv,socklen_t servlen,int flags);
,因 为res指针原本是没有指向一块合法内存的,所以,getaddrinfo调用结束后,我们必须使用如下配对函数来释放这块内存 #include<netdb.h> void freeaddrinfo(struct addrinfo*res);
### getnameinfo
能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数) 和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的gethostbyaddr和 getservbyport函数是否是它们的可重入版本
```c
#include<netdb.h>
int getnameinfo(const struct sockaddr*sockaddr,socklen_t addrlen,char*host,socklen_t hostlen,char*serv,socklen_t servlen,int flags);
//getnameinfo将返回的主机名存储在host参数指向的缓存中,将服务名存储在serv参数指向的缓存中, hostlen和servlen参数分别指定这两块缓存的长度。flags参数控制getnameinfo的行为
|