1、主机字节序和网络字节序
大端字节序:一个整数的高位字节(23-31bit)存储在内存的低地址处,低位字节(0~7bit)存储在内存的高地址处
小端字节序:相反
现代PC多采用小端字节序(主机字节序)
//测试本机是大端还是小端
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("little endian\n");
}
else
{
printf("unknown...\n");
}
}
Linux提供了四个函数完成切换
htonl表示将长整型(32bit)的主机字节序转换为网络字节序数据。长整型常用来转换IP地址,短整型用来转换端口号
2、专用socket地址
TCP/IP协议族有sockaddr_in等专用的socket地址结构体
IPV4:
struct sockaddr_in
{
sa_family_t sin_family; /*地址族:AF_INET*/
u_int16_t sin_port; /*端口号,要用网络字节序表示*/
struct in_addr sin_addr;/*IPv4地址结构体*/
};
struct in_addr
{
u_int32_t s_addr; /*用网络字节序表示*/
};
专用socket地址类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换),因为所有socket变成接口使用的地址参数类型都是sockaddr
IP地址转换函数
inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的地址
inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储在参数inp指向的地址结构中,成功返回1,否则0
inet_ntoa将网络字节序表示的地址转化为用点分十进制字符串表示的地址,该函数的返回值只想一个静态内存,因此inet_ntoa是不可重入的。
inet_pton函数将用字符串表示的IP地址src转换成用网络字节序整数表示的IP地址,并把转换结果存储于dst指向的内存中。af指定地址族,成功返回1,失败返回0设置errno
inet_ntop函数进行相反的转换,最后一个参数cnt指定目标存储单元的大小,下面两个宏能够帮助我们指定这个大小
成功时返回目标存储单元的地址,失败则返回NULL并设置errno
创建socket
命名socket
在服务器端程序中,通常要命名socket。客户端则不需要命名socket,而是采用匿名方式,使用操作系统自动分配的socket地址
bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen指出该socket地址的长度
bind成功时返回0,失败返回-1并设置errno。其中两种常见的errno是EACCES和EADDDRINUSE
EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务器端口(端口号为0~1023)上时,bind将返回EACCES错误
EADDRINUSE:被绑定的地址正在使用,比如将socket绑定到一个处于TIME_WAIT状态的socket地址
监听socket
socket被命名之后,还不能马上接受客户端连接,我们需要使用如下系统调用创建一个监听队列以存放待处理的客户连接
sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度,只表示处理完成连接状态的socket上限,典型值是5
backlog测试
/*接收3个参数:本机ip,端口,backlog*/
/*backlog测试程序*/
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>
static bool stop=false;
/*SIGNAL信号的处理函数,触发时结束主程序中的循环*/
static void handle_term(int sig)
{
stop=true;
}
int main(int argc,char* argv[])
{
signal(SIGTERM,handle_term);
if(argc<=3)
return 1;
const char* ip=argv[1];
int port=atoi(argv[2]);
int backlog=atoi(argv[3]);
int sock=socket(PF_INET,SOCK_STREAM,0);
assert(sock>=0);
struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family=AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);
address.sin_port=htons(port);
int ret=bind(sock,(struct sockaddr*)&address,sizeof(address));
ret=listen(sock,backlog);
assert(ret!=1);
while(!stop)
{
sleep(1);
}
close(sock);
return 0;
}
在其他主机或本机多次执行连接该程序
之后执行查看listen监听队列的内容
在监听队列中,处于ESTABLISHED状态的连接只有6个(backlog+1),其他的连接都处于SYN_RCVD状态。即完整连接最多有backlog+1个
接受连接
sockfd参数是执行过listen系统调用的监听socket,addr参数用来获取被接受连接的远端socket地址,该地址长度由addrlen参数指出。失败时返回-1并设置errno
接收一个异常连接
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
int main(int argc,char* argv[])
{
if(argc<=2)
return 1;
const char* ip=argv[1];
int port=atoi(argv[2]);
int sock=socket(PF_INET,SOCK_STREAM,0);
assert(sock>=0);
struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family=AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);
address.sin_port=htons(port);
int ret=bind(sock,(struct sockaddr*)&address,sizeof(address));
ret=listen(sock,5);
assert(ret!=1);
/*暂停20秒等待客户端连接和相关操作(掉线或退出)完成*/
sleep(20);
struct sockaddr_in client;
socklen_t client_addrlength=sizeof(client);
int connfd=accept(sock,(struct sockaddr*)&client,&client_addrlength);
if(connfd<0)
printf("errno is: %d\n",errno);
else
{
char remote[INET_ADDRSTRLEN];
printf("connected with ip:%s and port: %d\n",inet_ntop(AF_INET,
&client.sin_addr,remote,INET_ADDRSTRLEN),ntohs(client.sin_port));
}
close(sock);
return 0;
}
accept只是从监听队列中取出连接,而不论连接处于何种状态(EST和CLOSE_WAIT),更不关心任何网络状况的变化
发起连接
serv_addr参数是服务器监听的socket地址,addrlen参数则指定这个地址的长度。
关闭连接
close函数并非总是立即关闭一个连接,而是将fd的引用计数-1.只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数+1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭
如果无论如何都要立即终止连接,可以使用shutdown系统调用
sockfd参数是待关闭的socket,howto参数决定了shutdown的行为:
成功时返回0,否则-1