Linux socket 编程入门.docx
《Linux socket 编程入门.docx》由会员分享,可在线阅读,更多相关《Linux socket 编程入门.docx(11页珍藏版)》请在冰豆网上搜索。
Linuxsocket编程入门
Linuxsocket-编程入门(TCPserver端)
通常,socket编程总是Client/Server形式的,因为有了telnet,先不考虑client的程序,先写一个支持TCP协议的server端,然后用telnet作为client验证我们的程序。
TCPserver端的基本流程
想象你自己是个小大佬,坐办公室(什么样的黑社会做办公室啊?
可能是讨债公司吧^^)你很土,只有一个小弟帮你接电话(因为你自己的号码是不敢对外公开的)。
一次通讯的流程大概应该是这样的:
小弟那里的总机电话响了;小弟接起电话;对方说是你女朋友A妹;小弟转达说,“老大,你马子电话”;你说,接过来;小弟把电话接给你;你和你女朋友聊天半小时;挂电话。
分析一下整个过程中的元素。
你小弟(listenSock),你需要他来监听(listen)电话;你自己(communicationSock),实际上打电话进行交流的是你自己;你的电话号码(servAddr),否则你女朋友怎么能找到你?
你女朋友的电话号码(clntAddr),这个比喻有点牵强,因为事实上你接起电话,不需要知道对方的号码也可以通话(虽然事实上你应该是知道的,你不会取消了来电显示功能吧^^),但是,难道你是只接女朋友电话从来不打过去的牛人吗?
这个过程中的行为(成员函数):
你小弟接电话并转接给你(isAccept());你自己的通话(handleEcho())(这个行为确实比较土,只会乌鸦学舌的echo,呵呵)。
UNIX中的一切事物都是文件(everythinginUnixisafile!
)
这是UNIX的基本理念之一,也是一句很好的概括。
比如,很多UNIX老鸟会举出个例子来,“你看,/dev/hdc是个文件,它实际上也是我的光盘……”UNIX中的文件可以是:
网络连接(networkconnection),输入输出(FIFO),管道(apipe),终端(terminal),硬盘上的实际文件,或者其它任何东东。
3个已经打开的fd,0:
标准输入(STDIN_FILENO);1:
标准输出(STDOUT_FILENO);2:
标准错误(STDERR_FILENO)。
(以上宏定义在中)一个最简单的使用fd的例子,就是使用中的函数:
write(1,"Hello,World!
\n",20);,在标准输出上显示“Hello,World!
”。
file和fd并非一定是一一对应的。
当一个file被多个程序调用的时候,会生成相互独立的fd。
这个概念可以类比于C++中的引用(eg:
int&rTmp=tmp;)。
socket与filedescriptor
文件是应用程序与系统(包括特定硬件设备)之间的桥梁,而文件描述符就是应用程序使用这个“桥梁”的接口。
在需要的时候,应用程序会向系统申请一个文件,然后将文件的描述符返回供程序使用。
返回socket的文件通常被创建在/tmp或者/usr/tmp中。
我们实际上不用关心这些文件,仅仅能够利用返回的socket描述符就可以了。
收件人:
全体女生。
地址:
<一种地址描述方式>
事实上,在socket的通用address描述结构sockaddr中,正是用这样的方式来进行地址描述的:
struct sockaddr
{
unsigned short sa_family;
char sa_data[14];
};
sa_family可以认为是socketaddressfamily的缩写,也可能被简写成AF(AddressFamily),他就好像我们例子中那个“收件人:
全体女生”一样,虽然事实上有很多AF的种类,但是我们这个教程中只用得上大名鼎鼎的internet家族AF_INET。
另外的14字节是用来描述地址的。
这是一种通用结构,事实上,当我们指定sa_family=AF_INET之后,sa_data的形式也就被固定了下来:
最前端的2字节用于记录16位的端口,紧接着的4字节用于记录32位的IP地址,最后的8字节清空为零。
这就是我们实际在构造sockaddr时候用到的结构sockaddr_in(意指socketaddressinternet):
struct sockaddr_in
{
unsigned short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
我想,sin_的意思,就是socket(address)internet吧,只不过把address省略掉了。
sin_addr被定义成了一个结构,这个结构实际上就是:
structin_addr
{
unsignedlongs_addr;
};
in_addr显然是internetaddress了,s_addr是什么意思呢?
说实话我没猜出值得肯定的答案,也许就是socketaddress的意思吧,尽管跟更广义的sockaddr结构意思有所重复了。
哎,这些都是历史原因,也许我是没有精力去考究了。
原文介绍Linux的实现
看看源码实现:
Code
//Filename:
TcpServerClass.hpp
#ifndef TCPSERVERCLASS_HPP_INCLUDED
#define TCPSERVERCLASS_HPP_INCLUDED
#include
#include
#include
#include
class TcpServer
{
private:
int listenSock;
int communicationSock;
sockaddr_in servAddr;
sockaddr_in clntAddr;
public:
TcpServer(int listen_port);
bool isAccept();
void handleEcho();
};
#endif // TCPSERVERCLASS_HPP_INCLUDED
TcpServer:
:
TcpServer(int listen_port) // 创建了listen socket(监听嵌套字)
{
if ( (listenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ) {
throw "socket() failed";
}
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(listen_port);
if ( bind(listenSock, (sockaddr*)&servAddr, sizeof(servAddr)) < 0 ) {
throw "bind() failed";
}
if ( listen(listenSock, 10) < 0 ) {
throw "listen() failed";
}
}
bool TcpServer:
:
isAccept()
{
unsigned int clntAddrLen = sizeof(clntAddr);
if ( (communicationSock = accept(listenSock, (sockaddr*)&clntAddr, &clntAddrLen)) < 0 ) {
return false;
} else {
std:
:
cout << "Client(IP:
" << inet_ntoa(clntAddr.sin_addr) << ") connected.\n";
return true;
}
}
void TcpServer:
:
handleEcho()
{
const int BUFFERSIZE = 32;
char buffer[BUFFERSIZE];
int recvMsgSize;
bool goon = true;
while ( goon == true ) {
if ( (recvMsgSize = recv(communicationSock, buffer, BUFFERSIZE, 0)) < 0 ) {
throw "recv() failed";
} else if ( recvMsgSize == 0 ) {
goon = false;
} else {
if ( send(communicationSock, buffer, recvMsgSize, 0) !
= recvMsgSize ) {
throw "send() failed";
}
}
}
close(communicationSock);
}
//Filename:
main.cpp
//Tcp Server C++ style, single work
#include
#include "TcpServerClass.hpp"
int echo_server(int argc, char* argv[]);
int main(int argc, char* argv[])
{
int mainRtn = 0;
try {
mainRtn = echo_server(argc, argv);
}
catch ( const char* s ) {
perror(s);
exit(EXIT_FAILURE);
}
return mainRtn;
}
int echo_server(int argc, char* argv[])
{
int port;
if ( argc == 2 ) {
port = atoi(argv[1]);
} else {
port = 5000;
}
TcpServer myServ(port);
while ( true ) {
if ( myServ.isAccept() == true ) {
myServ.handleEcho();
}
}
return 0;
}
我们前面说到了网络分层:
链路——网络——传输——应用。
数据从应用程序里诞生,传送到互联网上每一层都会进行一次封装:
Data>>Application>>TCP/UDP>>IP>>OS(Driver,Kernel&PhysicalAddress)
我们用socket重点描述的是协议,包括网络协议(IP)和传输协议(TCP/UDP)。
sockaddr重点描述的是地址,包括IP地址和TCP/UDP端口。
socket()函数
我们从TcpServer:
:
TcpServer()函数可以看到,socket和sockaddr的产生是可以相互独立的。
socket()的函数原型是:
int socket(int protocolFamily, int type, int protocol);
在Linux中的实现为:
#include
/* Create a new socket of type TYPE in domain DOMAIN, using
protocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically.
Returns a file descriptor for the new socket, or -1 for errors. */
extern int socket (int __domain, int __type, int __protocol) __THROW;
第一个参数是协议簇(Linux里面叫作域,意思一样的),还是那句话,我们这篇教程用到的就仅仅是一个PF_INET(protocolfamily:
internet),很多时候你会发现人们也经常在这里赋值为AF_INET,事实上,当前,AF_INET就是PF_INET的一个#define,但是,写成PF_INET从语义上会更加严谨。
这也就是TCP/IP协议簇中的IP协议(InternetProtocol),网络层的协议。
后面两个参数定义传输层的协议。
第二个参数是传输层协议类型,我们教程里用到的宏,只有两个:
SOCK_STREAM(数据流格式)和SOCK_DGRAM(数据报格式);(具体是什么我们以后讨论)
第三个参数是具体的传输层协议。
当赋值为0的时候,系统会根据传输层协议类型自动匹配和选择。
事实上,当前,匹配SOCK_STREAM的就是TCP协议;而匹配SOCK_DGRAM就是UDP协议。
所以,我们指定了第二个参数,第三个就可以简单的设置为0。
不过,为了严谨,我们最好还是把具体协议写出来,比如,我们的例子中的TCP协议的宏名称:
IPPROTO_TCP。
数据的“地址”
从数据封装的模型,我们可以看到数据是怎么从应用程序传递到互联网的。
我们说过,数据的传送是通过socket进行的。
但是socket只描述了协议类型。
要让数据正确的传送到某个地方,必须添加那个地方的sockaddr地址;同样,要能接受网络上的数据,必须有自己的sockaddr地址。
可见,在网络上传送的数据包,是socket和sockaddr共同“染指”的结果。
他们共同封装和指定了一个数据包的网络协议(IP)和IP地址,传输协议(TCP/UDP)和端口号。
网络字节和本机字节的相互转换
sockaddr结构中的IP地址(sin_addr.s_addr)和端口号(sin_port)将被封装到网络上传送的数据包中,所以,它的结构形式需要保证是网络字节形式。
我们这里用到的函数是htons()和htonl(),这些缩写的意思是:
h:
host,主机(本机)
n:
network,网络
to:
to转换
s:
short,16位(2字节,常用于端口号)
l:
long, 32位(4字节,常用于IP地址)
“反过来”的函数也是存在的ntohs()和ntohl()。
socket和sockaddr的创建是可以相互独立的
首先通过socket()系统调用创建了listenSock,然后通过为结构体赋值的方法具体定义了服务器端的sockaddr。
这里需要补充的是说明宏定义INADDR_ANY。
这里的意思是使用本机所有可用的IP地址。
当然,如果你机器绑定了多个IP地址,你也可以指定使用哪一个。
socket与本机sockaddr的绑定
有时候绑定是系统的任务,特别是当你不需要知道自己的IP地址和所使用的端口号的时候。
但是,我们现在是建立服务器,你必须告诉客户端你的连接信息:
IP和Port。
所以,我们需要指明IP和Port,然后进行绑定。
intbind(intsocket,structsockaddr*localAddress,unsignedintaddressLength);
作为C++的程序员,也许你会觉得这个函数很不友好,它似乎更应该写成:
intbind_cpp_style(intsocket,constsockaddr&localAddress);
我们需要通过函数原型指明两点:
1、我们仅仅使用sockaddr结构的数据,但并不会对原有的数据进行修改;
2、我们使用的是完整的结构体,而不仅仅是这个结构体的指针。
(很显然光用指针是无法说明结构体大小的)
幸运的是,在Linux的实现中,这个函数已经被写为:
#include
/*GivethesocketFDthelocaladdressADDR(whichisLENbyteslong). */
externintbind(int__fd,__CONST_SOCKADDR_ARG__addr,socklen_t__len)
__THROW;
看到亲切的const,我们就知道这个指针带入是没有“副作用”的。
监听:
listen()
stream流模型形式上是一种“持续性”的连接,这就是要求信息的流动是“可来可去”的。
也就是说,stream流的socket除了绑定本机的sockaddr,还应该拥有对方sockaddr的信息。
在listen()中,这“对方的sockaddr”就可以不是某一个特定的sockaddr。
实际上,listensocket的目的是准备被动的接受来自“所有”sockaddr的请求。
所以,listen()反而就不能指定某个特定的sockaddr。
intlisten(intsocket,intqueueLimit);
其中第二个参数是等待队列的限制,一般设置在5-20。
Linux中实现为:
#include
/*PreparetoacceptconnectionsonsocketFD.
Nconnectionrequestswillbequeuedbeforefurtherrequestsarerefused.
Returns0onsuccess,-1forerrors. */
externintlisten(int__fd,int__n)__THROW;
完成了这一步,回到我们的例子,就像是让你小弟在电话机前做好了接电话的准备工作。
需要再次强调的是,这些行为仅仅是改变了socket的状态,实际上我想强调的是,为什么这些函数不会造成block(阻塞)的原因。
(block的概念以后再解释)
创建“通讯”嵌套字
这里的“通讯”加上了引号,是因为实际上所有的socket都有通讯的功能,只是在我们的例子中,之前那个socket只负责listen,而这个socket负责接受信息并echo回去。
我们现看看这个函数:
bool TcpServer:
:
isAccept()
{
unsigned int clntAddrLen = sizeof(clntAddr);
if ( (communicationSock = accept(listenSock, (sockaddr*)&clntAddr, &clntAddrLen)) < 0 ) {
return false;
} else {
std:
:
cout << "Client(IP:
" << inet_ntoa(clntAddr.sin_addr) << ") connected.\n";
return true;
}
}
用accept()创建新的socket
在我们的例子中,communicationSock实际上是用函数accept()创建的。
intaccept(intsocket,structsockaddr*clientAddress,unsignedint*addressLength);
在Linux中的实现为:
/*AwaitaconnectiononsocketFD.
Whenaconnectionarrives,openanewsockettocommunicatewithit,
set*ADDR(whichis*ADDR_LENbyteslong)totheaddressoftheconnecting
peerand*ADDR_LENtotheaddress'sactuallength,andreturnthe
newsocket'sdescriptor,or-1forerrors.
Thisfunctionisacancellationpointandthereforenotmarkedwith
__THROW. */
externintaccept(int__fd,__SOCKADDR_ARG__addr,
socklen_t*__restrict__addr_len);
这个函数实际上起着构造socket作用的仅仅只有第一个参数(另外还有一个不在这个函数内表现出来的因素,后面会讨论到),后面两个指针都有副作用,在socket创建后,会将客户端sockaddr的数据以及结构体的大小传回。
当程序调用accept()的时候,程序有可能就停下来等accept()的结果。
这就是我们前一小节说到的block(阻塞)。
这如同我们调用std:
:
cin的时候系统会等待输入直到回车一样。
accept()是一个有可能引起block的函数。
请注意我说的是“有可能”,这是因为accept()的block与否实际上决定与第一个参数socket的属性。
这个文件描述符如果是block的,accept()就block,否则就不block。
默认情况下,socket的属性是“可读可写”,并且,是阻塞的。
所以,我们不修改socket属性的时候,accept()是阻塞的。
accept()的另一面connect()
accept()只是在server端被动的等待,它所响应的,是client端connect()函数:
intconnect(intsocket,structsockaddr*foreignAddress,unsignedintaddressLength);
虽然我们这里不打算详细说明这个client端的函数,但是我们可以看出来,这个函数与之前我们介绍的bind()有几分相似,特别在Linux的实现中:
/*OpenaconnectiononsocketFD