@linux高性能服务器编程阅读笔记/socket地址API
一、字节序问题
1字节=8位二进制,也就是两位的16进制,例如
11111111B=0xFFH
计算机中一个内存地址可以存放8位1个字节的数据,以一个16位的2字节整形数据,在存取这2个字节时,由于大端字节序和小端字节序的不同,我们就需要考虑4个字节取出的顺序。其中大端把高位字节存放内存低地址处,小端把高位字节放在内存高地址处。,例如采取小端存储时,对于0x0F1F的一个数据,低位字节1F存放在内存低地址处,高位字节0F存放在内存高地址处。
现代PC一般使用小端字节序,而在网络传输中一般使用大端字节序。因此接收方在接受数据时,需要采用一些函数将数据转换成自己需要的类型。
二、基础知识
socket地址结构体中存放的是socket通信用的地址,公用socket地址结构体sockaddr和sockaddr_storage不够方便,在获取ip地址和端口时都需要进行位操作。因此专用socket地址结构体便产生了。下面是ipv4示例socksddr_in,在使用时还是要强制类型转换为sockaddr。
tcp连接状态与socket编程对应状态可参考下面这张图,三次握手,四次挥手
1.listen监听服务端socket收到的客户端连接请求
下面测试监听socket,我准备了两台ubuntu虚拟机,fei作为服务器安装了squid代理服务器,wang作为客户端,使用telnet命令连接服务器。
下面是在服务器端编写的一个简单的socket监听程序testsocket.cpp,用于监听客户端的连接请求。
#include<iostream>
using namespace std;
#include<sys/socket.h>
#include<signal.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
static bool stop=false;
static void handle_term(int sig)
{
stop=true;
}
int main()
{
//信号句柄
signal(SIGTERM,handle_term);
//定义监听地址
const char* ip="192.168.8.109";
int port=12345;
int backlog=5;
//创建socket
int sock=socket(AF_INET,SOCK_STREAM,0);
//创建socket地址结构体
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);
//绑定socket和socket地址结构体
int ret=bind(sock,(struct sockaddr*)&address,sizeof(address));
//监听socket地址结构体指定的IP地址个端口,一般就是服务器自己的ip地址。
ret=listen(sock,backlog);
while(!stop)
{
sleep(1);
}
close(sock);
return 0;
}
在服务器fei编译生成test程序,执行命令:./test,运行监听程序
在客户端wang执行命令:telnet 192.168.3.109 12345,向服务器建立tcp连接,执行多次。
在客户端执行命令:netstat -nt |grep 12345
可以观察到已经建立的tcp连接(estalished),由于listen监听队列已满(队列大小可以再listen函数中设置),因此有一个tcp连接没有成功建立。
2.接受服务端收到的客户端连接请求accept
下面测试服务端接受socket连接,并返回请求连接的客户端地址,在服务端编写testsocket1.cpp,
#include<iostream>
using namespace std;
#include<sys/socket.h>
#include<netinet/in.h>
#include<signal.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
int main()
{
const char* ip="192.168.8.109";
int port=12345;
int backlog=5;
int sock=socket(AF_INET,SOCK_STREAM,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);
struct sockaddr_in client;
socklen_t client_len=sizeof(client);
int ret1=accept(sock,(struct sockaddr*)&client,&client_len);
if(ret1>=0)
{
char remote[100];
printf("ip:%s\n",inet_ntop(AF_INET,&client.sin_addr,remote,100));
close(ret1);
}
close(sock);
return 0;
在服务端执行此程序,在客户端执行telnet命令,服务端程序输出请求连接的客户端的IP地址。结果如下图。
查看tcp传输的状态,由于打印ip之后关闭了监听的socket,断开连接,四次挥手,客户端状态变成了TIME_WAIT。
3.服务端可以通过listen被动监听客户端的连接,客户端则可以通过connect主动请求与服务端连接
下面是客户端请求连接的测试程序client.cpp,连接成功则打印connect success。
#include<iostream>
using namespace std;
#include<sys/socket.h>
#include<signal.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
int main()
{
const char* ip="192.168.8.109";
int port=12345;
int sockserver=socket(AF_INET,SOCK_STREAM,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=connect(sockserver,(struct sockaddr*)&address,sizeof(address));
if(ret==0)
{
cout<<"connect success"<<endl;
}
close(sockserver);
return 0;
}
前面在客户端执行telnet命令,系统会自动帮助我们向服务端发送连接请求,但是在这个程序中,我们直接自己申请连接服务端。
在客户端编写此程序并编译,在服务端首先运行我们前面写好的监听程序,再到客户端中运行连接程序,结果如下:
可以看到客户端通过这个程序也与服务端建立了一个tcp连接,由于最后我们关闭了客户端的socket,因此tcp连接结束,变为time_wait状态,要等待一段时间(大概是2MSL,MSL:报文最大生存时间)才会变为closed状态。
需要说明的是,为什么不是立即变成closed状态呢?有两个原因:
第一个是为了接收到服务端发送来的结束连接报文,并给以确认。
第二个是为了避免其他应用程序立即建立连接生成一个连接的化身(同ip与端口),会接收到原链接的数据报。原则上一个tcp端口是不能被打开多次的。
4.测试客户端send函数和服务端recv函数
首先是客户端程序send.cpp
#include<sys/socket.h>
#include<signal.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
int main()
{
const char* ip="192.168.8.109";
int port=12345;
int sockclient=socket(AF_INET,SOCK_STREAM,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=connect(sockclient,(struct sockaddr*)&address,sizeof(address));
if(ret==0)
{
const char* data1="abc";
const char*data2="12";
int ret1=send(sockclient,data1,strlen(data1),0);
if(ret1!=-1)
{
cout<<"send "<<ret1<<" byte successful"<<endl;
}
int ret2=send(sockclient,data2,strlen(data2),0);
if(ret2!=-1)
{
cout<<"send "<<ret2<<" byte successful"<<endl;
}
}
close(sockclient);
return 0;
}
然后是服务端程序recv.cpp
#include<iostream>
using namespace std;
#include<sys/socket.h>
#include<netinet/in.h>
#include<signal.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
int main()
{
const char* ip="192.168.8.109";
int port=12345;
int backlog=5;
int sock=socket(AF_INET,SOCK_STREAM,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);
struct sockaddr_in client_address;
socklen_t client_len=sizeof(client_address);
int client_socket=accept(sock,(struct sockaddr*)&client_address,&client_len);
if(client_socket>=0)
{
sleep(10);//测试使用
char buffer[1024]={0};
recv(client_socket,buffer,1024,0);
cout<<"recv:"<<buffer<<endl;
recv(client_socket,buffer,1024,0);
cout<<"recv:"<<buffer<<endl;
}
close(sock);
return 0;
}
客户端输出结果:
服务端输出结果:
客户端发送了两次数据,服务端接受了两次数据,分别做了一下几次试验:
1)两次send不加延时,recv两次接受也不加延时,此时接受的数据分别是abc,12c。
2)两次send不加延时,recv两次接受之前加10s延时,此时接受的数据分别是abc12,abc12。如上面的程序。
3)两次send之间加10s延时,recv两次接受之间不加延时,此时接受的数据分别是abc,12c。
可以看出:
0.recv属于一被调用就去读缓冲区中的数据。send一被调用就会将数据发送缓冲区。
1.send函数和recv函数只是并不直接参与网络数据的传输,send只是将数据送到内核发送缓冲区,recv只是将数据从内核接收缓冲区读取出来。真正在网络
2.对于实验1),第一次send发送abc,recv立马接收到abc,第二次send12,由于abc已经被recv读取,所以12会从头写入缓冲区覆盖掉ab,变成12c。
3.对于实验2),第一次send发送abc,recv由于延时10s,数据没有被接收,第二次send12,recv等到延时结束,此时两次都从从缓冲区中读取abc12。
4.对于实验3),第一次send发送abc,recv立马接收到abc,第二次send覆盖缓冲区,读取成为12c。
5.send和recv都是阻塞的,函数调用但不会立刻返回,需要等待操作完成。
5.sendfile函数的使用,从两个描述符之间高速拷贝数据,读取的是真实的文件描述符,写入的是socket的描述符。利用sendfile函数将服务端一个文件发送到客户端。
下面的是sendfile测试程序。
#include<iostream>
using namespace std;
#include<sys/socket.h>
#include<netinet/in.h>
#include<signal.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/sendfile.h>
int main()
{
const char* ip="192.168.8.109";
int port=12345;
int backlog=5;
int filefd=open("recv.cpp",O_RDONLY);
struct stat stat_buf;
fstat(filefd,&stat_buf);
int sock=socket(AF_INET,SOCK_STREAM,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);
struct sockaddr_in client_address;
socklen_t client_len=sizeof(client_address);
int client_socket=accept(sock,(struct sockaddr*)&client_address,&client_len);
if(client_socket>=0)
{
cout<<"start"<<endl;
sendfile(client_socket,filefd,NULL,stat_buf.st_size);
cout<<"end"<<endl;
close(client_socket);
}
close(sock);
return 0;
}
服务端编译运行,客户端telnet 192.168.8.109 12345,会展现发送过来的文件。
6.dup函数会复制产生一个新的描述符,但是描述符的值是当前最小的空闲值,但是指向的还是原来的那个文件,以此实现CGI服务器。
具体做法就是先关掉标准输出文件符STDOUT_FILENO(值为1),再dup客户端socket描述符,新生成的描述符值将为1,当调用输出语法printf和cout时,输出将定位到复制的客户端socket描述符中。
#include<iostream>
using namespace std;
#include<sys/socket.h>
#include<netinet/in.h>
#include<signal.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/sendfile.h>
int main()
{
const char* ip="192.168.0.109";
int port=12345;
int backlog=5;
int filefd=open("recv.cpp",O_RDONLY);
struct stat stat_buf;
fstat(filefd,&stat_buf);
int sock=socket(AF_INET,SOCK_STREAM,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);
struct sockaddr_in client_address;
socklen_t client_len=sizeof(client_address);
int client_socket=accept(sock,(struct sockaddr*)&client_address,&client_len);
if(client_socket>=0)
{
cout<<"start"<<endl;
close(STDOUT_FILENO);
dup(client_socket);
printf("abc\n");
cout<<"end"<<endl;
close(client_socket);
}
close(sock);
return 0;
}
客户端telnet如下图,printf和cout的输出结果都进入了客户端socket描述符中。
7.splice函数也是零拷贝在描述符之间移动数据,且有一个必须是管道描述符。
由splice函数实现一个回射服务器,将客户端发送的数据返回去。
服务端splice.cpp的代码:
#include<iostream>
using namespace std;
#include<sys/socket.h>
#include<netinet/in.h>
#include<signal.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/sendfile.h>
int main()
{
const char* ip="192.168.0.109";
int port=12345;
int backlog=5;
int sock=socket(AF_INET,SOCK_STREAM,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);
struct sockaddr_in client_address;
socklen_t client_len=sizeof(client_address);
int client_socket=accept(sock,(struct sockaddr*)&client_address,&client_len);
if(client_socket>=0)
{
cout<<"start"<<endl;
int pipefd[2];
ret=pipe(pipefd);
ret=splice(client_socket,NULL,pipefd[1],NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
ret=splice(pipefd[0],NULL,client_socket,NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
close(client_socket);
}
close(sock);
return 0;
}
客户端splice_.cpp的代码:
#include<iostream>
using namespace std;
#include<sys/socket.h>
#include<signal.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
int main()
{
const char* ip="192.168.0.109";
int port=12345;
int sockclient=socket(AF_INET,SOCK_STREAM,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=connect(sockclient,(struct sockaddr*)&address,sizeof(address));
if(ret==0)
{
const char* data1="abc";
send(sockclient,data1,strlen(data1),0);
char buffer[1024];
recv(sockclient,buffer,1024,0);
cout<<buffer<<endl;
}
close(sockclient);
return 0;
}
运行结果如下图,发送给服务端的数据通过pipe管道和splice函数重新回到了客户端socket的描述符中。
8.tee函数实现从键盘输入到屏幕和文件,tee函数接受的只能是两个管道描述符。
STDIN_FILENO—键盘输入
STDOUT_FILENO—屏幕输出
下面是tee.cpp的代码
#include<iostream>
using namespace std;
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int filefd=open("tee.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
int pipefd_stdout[2];
pipe(pipefd_stdout);
int pipefd_file[2];
pipe(pipefd_file);
cout<<"123"<<endl;
int ret=splice(STDIN_FILENO,NULL,pipefd_stdout[1],NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
tee(pipefd_stdout[0],pipefd_file[1],32768,SPLICE_F_NONBLOCK);
splice(pipefd_file[0],NULL,filefd,NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
splice(pipefd_stdout[0],NULL,STDOUT_FILENO,NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
close(filefd);
close(pipefd_stdout[0]);
close(pipefd_stdout[1]);
close(pipefd_file[0]);
close(pipefd_file[1]);
return 0;
}
执行结果如下图。键盘输入1,屏幕也打印1,并且将1写入到了tee.txt中。