Linux下Socket编程之TCP Server端
2010年07月16日
一。建模
绝大部分关于socket编程的教程总是从socket的概念开始讲起的。要知道,socket的初衷是个庞大的体系,TCP/IP只是这个庞大体系下一个很小的子集,而我们真正能用上的更是这个子集中的一小部分:运输层(Host-to-Host Transport Layer)的TCP和UDP协议,以及使用这两个协议进行应用层(Application Layer)的开发。即使是socket的核心部分,网络层(Internet Layer)的IP协议,在编程的时候我们也很少会感觉到它的存在--因为已经被封装好了,我们唯一需要做的事情就是传入一个宏。第一节我想介绍的概念就这么多,当然,既然我们已经说了3个层了,我想最好还是把最后一个层也说出来,即所谓链路层(Network Access Layer),它包括了物理硬件和驱动程序。这四个层从底到高的顺序是:链路层--网络层--运输层--应用层。
好,说实话我们现在并不清楚所谓TCP到底是什么东东,不过我们知道这东东名气很大。或许你早就知道,另外一个声名狼藉建立在TCP协议基础上的应用程序,它曾经几乎是统治了一个时代,即使是今天,我们依然无法消除他的影响力的--恩,是的,就是telnet。
在这个教程中,我使用的环境是Debian GNU/Linux 4.0 etch。传说中的stable -_-!!!,恩,我是很保守的人。如果你不是自己DIY出来的系统,相信默认安装里面就应该有telnet(/usr/bin/telnet,要是没装就自己aptitude install吧)。telnet可以与所有遵循TCP协议的服务器端进行通讯。通常,socket编程总是Client/Server形式的,因为有了telnet,我们可以先不考虑client的程序,我们先写一个支持TCP协议的server端,然后用telnet作为client验证我们的程序就好了。
server端的功能,我们也考虑一种最简单的反馈形式:echo。就如同你在终端输入echo 'Hello World',回车后shell就会给你返回Hello World一样,我们的第一个TCP server就用以实现这个功能。
什么样的模型适合描述这样的一种server呢?我相信,一个很2的例子会有助于我们记忆TCP server端的基本流程。
想象你自己是个小大佬,坐办公室(什么样的黑社会做办公室啊?可能是讨债公司吧^^)你很土,只有一个小弟帮你接电话(因为你自己的号码是不敢对外公开的)。一次通讯的流程大概应该是这样的:小弟那里的总机电话响了;小弟接起电话;对方说是你女朋友A妹;小弟转达说,"老大,你马子电话";你说,接过来;小弟把电话接给你;你和你女朋友聊天半小时;挂电话。
我们来分析一下整个过程中的元素。先分析成员数据(请注意,这里开始用C++术语了):你小弟(listenSock),你需要他来监听(listen,这是socket编程中的术语)电话;你自己(communicationSock),实际上打电话进行交流的是你自己;你的电话号码(servAddr),否则你女朋友怎么能找到你?你女朋友的电话号码(clntAddr),这个比喻有点牵强,因为事实上你接起电话,不需要知道对方的号码也可以通话(虽然事实上你应该是知道的,你不会取消了来电显示功能吧^^),但是,难道你是只接女朋友电话从来不打过去的牛人吗?这个过程中的行为(成员函数):你小弟接电话并转接给你(isAccept());你自己的通话(handleEcho())(这个行为确实比较土,只会乌鸦学舌的echo,呵呵)。
简单的说,就是这些了。根据这个模型,我们可以很容易写出实现我们需要的echo功能的TCP server的类:
这里面有些简写,比如,sock实际上就是socket,addr就是address。serv和clnt我想你一定能猜到是server和client吧。还有一个socket中的结构体sockaddr_in,实际上就是这个意思:socket address internet(网络嵌套字地址),具体解说,请看下回分解。
二。socket与文件描述符 UNIX中的一切事物都是文件(everything in Unix is a file!)
当我在这篇教程中提到UNIX的时候,其意思专指符合UNIX标准的所谓"正统"UNIX的衍生系统(其实我就用来带指那些买了最初UNIX源代码的商业系统)操作系统和类似Linux,BSD这些类UNIX系统。如果某些要点是Linux特有的,或者因为本人孤陋寡闻暂时搞不清楚是Linux特有的还是UNIX通用的,我就会指明是Linux,甚至其发行版(我本人在写这篇教程的时候是以Debian GNU/Linux 4.0 etch为测试平台的)。
我们学习UNIX的时候,恐怕听到的第一句话就是这句:UNIX中一切都是文件。这是UNIX的基本理念之一,也是一句很好的概括。比如,很多UNIX老鸟会举出个例子来,"你看,/dev/hdc是个文件,它实际上也是我的光盘……"UNIX中的文件可以是:网络连接(network connection),输入输出(FIFO),管道(a pipe),终端(terminal),硬盘上的实际文件,或者其它任何东东。
文件与文件描述符(file & file descriptor)
你可能对上一章中建模类中的int还记忆犹新。我们用int在描述socket,实际上,所有的文件描述符都是int,没错,用的是一个整数类型。如果你觉得这样让你很难接受,那么恭喜你,你跟我一样,也许是深中C++面向对象思想的毒了^^。因为是int,所以文件描述符不可能是C++概念中的对象,因为int无法发出行为,但是,这并不代表也不能接受一个动作哈。
PASCAL之父在批判面向对象思想教条的时候,曾经生动的举了个例子,"在OOP的概念中,绝对不应该接受a+b这种表达的, OOP对这个问题的表达应该是a.add(b)"。fd(file descriptor)可以作为接受动作的对象,但是本身却无法发出动作,这就如同一个只能做宾语不能做主语的名词,是个不完整的对象。但是,请别忘了Linux和socket本身是C语言的产物,我们必须接受在面向过程时代下的产物,正视历史--当然,这与我们自己再进行OOP的封装并不矛盾。
我们应该记住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与file descriptor
文件是应用程序与系统(包括特定硬件设备)之间的桥梁,而文件描述符就是应用程序使用这个"桥梁"的接口。在需要的时候,应用程序会向系统申请一个文件,然后将文件的描述符返回供程序使用。返回socket的文件通常被创建在/tmp或者/usr/tmp中。我们实际上不用关心这些文件,仅仅能够利用返回的socket描述符就可以了。
好了,说了这么多,实际上就解释了一个问题,"为什么socket的类型是int?" -_-!!! 三。sockaddr与sockaddr_in 收件人地址
一家化妆品公司将一批新产品的样品,准备发给某学校某个班的女生们免费试用。通常情况下,这件邮包的地址上可以这么写:
收件人:全体女生。
地址:A省B市C学校,X级Y班。
收件人:全体女生。
地址:请打电话xxxxxxxx,找他们学校一个叫Lucy的女生,然后把东西送到她的班上。
这种文字是相当的诡异啊-_-!!!,但是并不等于就没有表述清楚邮包的去向和地址。事实上邮局看到这样的地址一定会发飙的,然而对于电脑,如果你的地址描述形式是他可以接受和执行的,他就会老老实实的按你的要求去做……
所以,如何描述地址不是问题的关键,关键在于这样的表述是不是能够表述清楚一个地址。一种更加通用的表达形式可能是这样的:
这是一个16字节大小的结构(2+14),sa_family可以认为是socket address family的缩写,也可能被简写成AF(Address Family),他就好像我们例子中那个"收件人:全体女生"一样,虽然事实上有很多AF的种类,但是我们这个教程中只用得上大名鼎鼎的internet家族AF_INET。另外的14字节是用来描述地址的。这是一种通用结构,事实上,当我们指定sa_family=AF_INET之后,sa_data的形式也就被固定了下来:最前端的2字节用于记录16位的端口,紧接着的4字节用于记录32位的IP地址,最后的8字节清空为零。这就是我们实际在构造sockaddr时候用到的结构sockaddr_in(意指socket address internet):
我想,sin_的意思,就是socket (address) internet吧,只不过把address省略掉了。sin_addr被定义成了一个结构,这个结构实际上就是:
in_addr显然是internet address了,s_addr是什么意思呢?说实话我没猜出值得肯定的答案(根据下面网友的评论,其意思为source address,谢谢),也许就是socket address的意思吧,尽管跟更广义的sockaddr结构意思有所重复了。哎,这些都是历史原因,也许我是没有精力去考究了。
sockaddr和sockaddr_in在Linux中的实现
你可能还记得我之前说过,UNIX和Linux上的socket实现都是从BSD的socket实现演变过来的。事实上,socket这个词本来的意思,就是Berkeley Socket interface的简单说法。Linux上的socket与原本的socket的应该是完全兼容的,不过发展到今天,在代码实现上可能有些小的差别。我们就吹毛求疵的来看看这些区别在什么地方。
#include
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
//==============
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;
/* This macro is used to declare the initial common members
of the data types used for socket addresses, `struct sockaddr',
`struct sockaddr_in', `struct sockaddr_un', etc. */
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
可以看到,转了几次typedef,几次宏定义,实际效果是与标准socket一样的。
#include
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
//=================
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
同样的,看起来挺复杂,实际上与标准socket的定义是一样的。
头文件依赖关系
是包含在中的,是包含在中的,实际上我们在程序中往往就是:
值得知道的是,ARPA是 Advanced research project agency(美国国防部高级研究计划暑)的所写,ARPANET是当今互联网的前身,所以我们就可以想象,为什么inet.h会在arpa目录下了。
四。构造函数涉及的概念
话题回到"黑社会办公室"的例子,讲概念已经扯得比较远了,不过,这一节我们还得讲概念,不过好在有些程序的例子。如果大家不想翻回去看TcpServer类的原型,我这里直接给出这个头文件的完整源代码:
//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
我们已经解释了为什么listenSock和communicationSock的类型是int,以及sockaddr_in是什么结构,现在来写这个类的构造函数:
TcpServer::TcpServer(int listen_port)
{
if ( (listenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) 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)) >Application>>TCP/UDP>>IP>>OS(Driver, Kernel & Physical Address)
我们用socket重点描述的是协议,包括网络协议(IP)和传输协议(TCP/UDP)。
sockaddr重点描述的是地址,包括IP地址和TCP/UDP端口。
socket()函数
我们从TcpServer::TcpServer()函数可以看到,socket和sockaddr的产生是可以相互独立的。socket()的函数原型是:
#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(protocol family : internet),很多时候你会发现人们也经常在这里赋值为AF_INET,事实上,当前,AF_INET就是PF_INET的一个#define,但是,写成PF_INET从语义上会更加严谨。这也就是TCP/IP协议簇中的IP协议(Internet Protocol),网络层的协议。
后面两个参数定义传输层的协议。
第二个参数是传输层协议类型,我们教程里用到的宏,只有两个: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()。
动作与持续行为
本节最后的一个概念可以跟计算机无关。作为动词,有些可以描述动作,有些是描述一重持续的行为状态的(就如同一般动词和be动词一样)。扯到C++来说,我们可以把持续行为封装到函数内部,只留出动作的接口。事实上,构造函数中的bind()和listen()就是这种描述持续状态的行为函数。
五。创建监听套接字
前面一小节,我们已经写出了TcpServer的构造函数。这个函数的实际作用,就是创建了listen socket(监听嵌套字)。这一节,我们来具体分析这个创建的过程。
socket和sockaddr的创建是可以相互独立的
在函数中,我们首先通过socket()系统调用创建了listenSock,然后通过为结构体赋值的方法具体定义了服务器端的sockaddr。(memset()函数的作用是把某个内存段的空间设定为某值,这里是清零。)其他的概念已经在前一小节讲完了。这里需要补充的是说明宏定义INADDR_ANY。这里的意思是使用本机所有可用的IP地址。当然,如果你机器绑定了多个IP地址,你也可以指定使用哪一个。
数据流简易模型(SOCK_STREAM)
我们的例子以电话做的比喻,实际上,socket stream模型不完全类似电话,它至少有以下这些特点:
1、一种持续性的连接。这点跟电话是类似的,也可以想象成流动着液体的水管。一旦断开,这种流动就会中断。
2、数据包的发送实际上是非连续的。这个世界上有什么事物是真正的线性连续的?呵呵,扯远了,这貌似一个哲学问题。我们仅仅需要知道的是,一个数据包不可能是无限大的,所以,总是一个小数据包一个小数据包这样的发送的。这一点,又有点像邮包的传递。这些数据包到达与否,到达的先后次序本身是无法保证的,即是说,是IP协议无法保证的。但是stream形式的TCP协议,在IP之上,做了一定到达和到达顺序的保证。
3、传送管道实际上是非封闭的。要不干嘛叫"网络"-_-!!!。我们之所以能保证数据包的"定点"传送,完全是依靠每个数据包都自带了目的地址信息。
由此可见,虽然socket和sockaddr可以分别创建,并无依赖关系。但是在实际使用的时候,一个socket至少会绑定一个本机的sockaddr,没有自己的"地址信息",就不能接受到网络上的数据包(至少在TCP协议里面是这样的)。
socket与本机sockaddr的绑定
有时候绑定是系统的任务,特别是当你不需要知道自己的IP地址和所使用的端口号的时候。但是,我们现在是建立服务器,你必须告诉客户端你的连接信息:IP和Port。所以,我们需要指明IP和Port,然后进行绑定。
作为C++的程序员,也许你会觉得这个函数很不友好,它似乎更应该写成:
我们需要通过函数原型指明两点:
1、我们仅仅使用sockaddr结构的数据,但并不会对原有的数据进行修改;
2、我们使用的是完整的结构体,而不仅仅是这个结构体的指针。(很显然光用指针是无法说明结构体大小的)
幸运的是,在Linux的实现中,这个函数已经被写为:
看到亲切的const,我们就知道这个指针带入是没有"副作用"的。
监听:listen()
stream流模型形式上是一种"持续性"的连接,这就是要求信息的流动是"可来可去"的。也就是说,stream流的socket除了绑定本机的sockaddr,还应该拥有对方sockaddr的信息。在listen()中,这"对方的sockaddr"就可以不是某一个特定的sockaddr。实际上,listen socket的目的是准备被动的接受来自"所有"sockaddr的请求。所以,listen()反而就不能指定某个特定的sockaddr。
其中第二个参数是等待队列的限制,一般设置在5-20。Linux中实现为:
完成了这一步,回到我们的例子,就像是让你小弟在电话机前做好了接电话的准备工作。需要再次强调的是,这些行为仅仅是改变了socket的状态,实际上我想强调的是,为什么这些函数不会造成block(阻塞)的原因。(block的概念以后再解释)
六。创建"通讯"套接字
这里的"通讯"加上了引号,是因为实际上所有的socket都有通讯的功能,只是在我们的例子中,之前那个socket只负责listen,而这个socket负责接受信息并echo回去。
我们现看看这个函数:
bool TcpServer::isAccept()
{
unsigned int clntAddrLen = sizeof(clntAddr);
if ( (communicationSock = accept(listenSock, (sockaddr*)&clntAddr, &clntAddrLen)) socket
在我们的例子中,communicationSock实际上是用函数accept()创建的。
/* Await a connection on socket FD.
When a connection arrives, open a new socket to communicate with it,
set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
peer and *ADDR_LEN to the address's actual length, and return the
new socket's descriptor, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int accept (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()函数:
虽然我们这里不打算详细说明这个client端的函数,但是我们可以看出来,这个函数与之前我们介绍的bind()有几分相似,特别在Linux的实现中:
/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
For connectionless socket types, just set the default address to send to
and the only address from which to accept transmissions.
Return 0 on success, -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
connect() 也使用了const的sockaddr,只不过是远程电脑上的而非bind()的本机。
accept()在server端表面上是通过listen socket创建了新的socket,实际上,这种行为是在接受对方客户机程序中connect()函数的请求后发生的。综合起看,被创建的新socket实际上包含了listen socket的信息以及客户端connect()请求中所包含的信息--客户端的sockaddr地址。
新socket与sockaddr的关系
accept()创建的新socket(我们例子中的communicationSock,这里我们简单用newSock来带指)首先包含了listen socket的信息,所以,newSock具有本机sockaddr的信息;其次,因为它响应于client端connect()函数的请求,所以,它还包含了clinet端sockaddr的信息。
我们说过,stream流形式的TCP协议实际上是建立起一个"可来可去"的通道。用于listen的通道,远程机的目标地址是不确定的;但是newSock却是有指定的本机地址和远程机地址,所以,这个socket,才是我们真正用于TCP"通讯"的socket。
inet_ntoa()
对于这个函数,我们可以作为一种,将IP地址,由in_addr结构转换为可读的ASCII形式的固定用法。
七。接受与发送
现在,我们通过accept()创建了新的socket,也就是我们类中的数据成员communicationSock,现在,我们就可以通过这个socket进行通讯了。
TCP通讯模型
在介绍函数之前,我们应该了解一些事实。TCP的Server/Client模型类似这样:
ServApp--ServSock--Internet--ClntSock--ClntApp
当然,我们这里的socket指的就是用于"通讯"的socket。TCP的server端至少有两个socket,一个用于监听,一个用于通讯;TCP的server端可以只有一个socket,这个socket同时"插"在server的两个socket上。当然,插上listen socket的目的只是为了创建communication socket,创建完备后,listen是可以关闭的。但是,如果这样,其他的client就无法再连接上server了。
我们这个模型,是client的socket插在server的communication socket上的示意。这两个socket,都拥有完整的本地地址信息以及远程计算机地址信息,所以,这两个socket以及之间的网络实际上形成了一条形式上"封闭"的管道。数据包只要从一端进来,就能知道出去的目的地,反之亦然。这正是TCP协议,数据流形式抽象化以及实现。因为不再需要指明"出处"和"去向",对这样的socket(实际上是S/C上的socket对)的操作,就如同对本地文件描述符的操作一样。但是,尽管我们可以使用read()和write(),但是,为了完美的控制,我们最好使用recv()和send()。
recv()和send()
#include
/* Send N bytes of BUF to socket FD. Returns the number sent or -1.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t send (int __fd, __const void *__buf, size_t __n, int __flags);
/* Read N bytes into BUF from socket FD.
Returns the number read or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);
这两个函数的第一个参数是用于"通讯"的socket,第二个参数是发送或者接收数据的起始点指针,第三个参数是数据长度,第四个参数是控制符号(默认属性设置为0就可以了)。失败时候传回-1,否则传回实际发送或者接收数据的大小,返回0往往意味着连接断开了。
处理echo行为
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))
#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
//Filename: TcpServerClass.cpp
#include "TcpServerClass.hpp"
TcpServer::TcpServer(int listen_port)
{
if ( (listenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) 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))
#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;
}
2010年07月16日
一。建模
绝大部分关于socket编程的教程总是从socket的概念开始讲起的。要知道,socket的初衷是个庞大的体系,TCP/IP只是这个庞大体系下一个很小的子集,而我们真正能用上的更是这个子集中的一小部分:运输层(Host-to-Host Transport Layer)的TCP和UDP协议,以及使用这两个协议进行应用层(Application Layer)的开发。即使是socket的核心部分,网络层(Internet Layer)的IP协议,在编程的时候我们也很少会感觉到它的存在--因为已经被封装好了,我们唯一需要做的事情就是传入一个宏。第一节我想介绍的概念就这么多,当然,既然我们已经说了3个层了,我想最好还是把最后一个层也说出来,即所谓链路层(Network Access Layer),它包括了物理硬件和驱动程序。这四个层从底到高的顺序是:链路层--网络层--运输层--应用层。
好,说实话我们现在并不清楚所谓TCP到底是什么东东,不过我们知道这东东名气很大。或许你早就知道,另外一个声名狼藉建立在TCP协议基础上的应用程序,它曾经几乎是统治了一个时代,即使是今天,我们依然无法消除他的影响力的--恩,是的,就是telnet。
在这个教程中,我使用的环境是Debian GNU/Linux 4.0 etch。传说中的stable -_-!!!,恩,我是很保守的人。如果你不是自己DIY出来的系统,相信默认安装里面就应该有telnet(/usr/bin/telnet,要是没装就自己aptitude install吧)。telnet可以与所有遵循TCP协议的服务器端进行通讯。通常,socket编程总是Client/Server形式的,因为有了telnet,我们可以先不考虑client的程序,我们先写一个支持TCP协议的server端,然后用telnet作为client验证我们的程序就好了。
server端的功能,我们也考虑一种最简单的反馈形式:echo。就如同你在终端输入echo 'Hello World',回车后shell就会给你返回Hello World一样,我们的第一个TCP server就用以实现这个功能。
什么样的模型适合描述这样的一种server呢?我相信,一个很2的例子会有助于我们记忆TCP server端的基本流程。
想象你自己是个小大佬,坐办公室(什么样的黑社会做办公室啊?可能是讨债公司吧^^)你很土,只有一个小弟帮你接电话(因为你自己的号码是不敢对外公开的)。一次通讯的流程大概应该是这样的:小弟那里的总机电话响了;小弟接起电话;对方说是你女朋友A妹;小弟转达说,"老大,你马子电话";你说,接过来;小弟把电话接给你;你和你女朋友聊天半小时;挂电话。
我们来分析一下整个过程中的元素。先分析成员数据(请注意,这里开始用C++术语了):你小弟(listenSock),你需要他来监听(listen,这是socket编程中的术语)电话;你自己(communicationSock),实际上打电话进行交流的是你自己;你的电话号码(servAddr),否则你女朋友怎么能找到你?你女朋友的电话号码(clntAddr),这个比喻有点牵强,因为事实上你接起电话,不需要知道对方的号码也可以通话(虽然事实上你应该是知道的,你不会取消了来电显示功能吧^^),但是,难道你是只接女朋友电话从来不打过去的牛人吗?这个过程中的行为(成员函数):你小弟接电话并转接给你(isAccept());你自己的通话(handleEcho())(这个行为确实比较土,只会乌鸦学舌的echo,呵呵)。
简单的说,就是这些了。根据这个模型,我们可以很容易写出实现我们需要的echo功能的TCP server的类:
这里面有些简写,比如,sock实际上就是socket,addr就是address。serv和clnt我想你一定能猜到是server和client吧。还有一个socket中的结构体sockaddr_in,实际上就是这个意思:socket address internet(网络嵌套字地址),具体解说,请看下回分解。
二。socket与文件描述符 UNIX中的一切事物都是文件(everything in Unix is a file!)
当我在这篇教程中提到UNIX的时候,其意思专指符合UNIX标准的所谓"正统"UNIX的衍生系统(其实我就用来带指那些买了最初UNIX源代码的商业系统)操作系统和类似Linux,BSD这些类UNIX系统。如果某些要点是Linux特有的,或者因为本人孤陋寡闻暂时搞不清楚是Linux特有的还是UNIX通用的,我就会指明是Linux,甚至其发行版(我本人在写这篇教程的时候是以Debian GNU/Linux 4.0 etch为测试平台的)。
我们学习UNIX的时候,恐怕听到的第一句话就是这句:UNIX中一切都是文件。这是UNIX的基本理念之一,也是一句很好的概括。比如,很多UNIX老鸟会举出个例子来,"你看,/dev/hdc是个文件,它实际上也是我的光盘……"UNIX中的文件可以是:网络连接(network connection),输入输出(FIFO),管道(a pipe),终端(terminal),硬盘上的实际文件,或者其它任何东东。
文件与文件描述符(file & file descriptor)
你可能对上一章中建模类中的int还记忆犹新。我们用int在描述socket,实际上,所有的文件描述符都是int,没错,用的是一个整数类型。如果你觉得这样让你很难接受,那么恭喜你,你跟我一样,也许是深中C++面向对象思想的毒了^^。因为是int,所以文件描述符不可能是C++概念中的对象,因为int无法发出行为,但是,这并不代表也不能接受一个动作哈。
PASCAL之父在批判面向对象思想教条的时候,曾经生动的举了个例子,"在OOP的概念中,绝对不应该接受a+b这种表达的, OOP对这个问题的表达应该是a.add(b)"。fd(file descriptor)可以作为接受动作的对象,但是本身却无法发出动作,这就如同一个只能做宾语不能做主语的名词,是个不完整的对象。但是,请别忘了Linux和socket本身是C语言的产物,我们必须接受在面向过程时代下的产物,正视历史--当然,这与我们自己再进行OOP的封装并不矛盾。
我们应该记住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与file descriptor
文件是应用程序与系统(包括特定硬件设备)之间的桥梁,而文件描述符就是应用程序使用这个"桥梁"的接口。在需要的时候,应用程序会向系统申请一个文件,然后将文件的描述符返回供程序使用。返回socket的文件通常被创建在/tmp或者/usr/tmp中。我们实际上不用关心这些文件,仅仅能够利用返回的socket描述符就可以了。
好了,说了这么多,实际上就解释了一个问题,"为什么socket的类型是int?" -_-!!! 三。sockaddr与sockaddr_in 收件人地址
一家化妆品公司将一批新产品的样品,准备发给某学校某个班的女生们免费试用。通常情况下,这件邮包的地址上可以这么写:
收件人:全体女生。
地址:A省B市C学校,X级Y班。
收件人:全体女生。
地址:请打电话xxxxxxxx,找他们学校一个叫Lucy的女生,然后把东西送到她的班上。
这种文字是相当的诡异啊-_-!!!,但是并不等于就没有表述清楚邮包的去向和地址。事实上邮局看到这样的地址一定会发飙的,然而对于电脑,如果你的地址描述形式是他可以接受和执行的,他就会老老实实的按你的要求去做……
所以,如何描述地址不是问题的关键,关键在于这样的表述是不是能够表述清楚一个地址。一种更加通用的表达形式可能是这样的:
这是一个16字节大小的结构(2+14),sa_family可以认为是socket address family的缩写,也可能被简写成AF(Address Family),他就好像我们例子中那个"收件人:全体女生"一样,虽然事实上有很多AF的种类,但是我们这个教程中只用得上大名鼎鼎的internet家族AF_INET。另外的14字节是用来描述地址的。这是一种通用结构,事实上,当我们指定sa_family=AF_INET之后,sa_data的形式也就被固定了下来:最前端的2字节用于记录16位的端口,紧接着的4字节用于记录32位的IP地址,最后的8字节清空为零。这就是我们实际在构造sockaddr时候用到的结构sockaddr_in(意指socket address internet):
我想,sin_的意思,就是socket (address) internet吧,只不过把address省略掉了。sin_addr被定义成了一个结构,这个结构实际上就是:
in_addr显然是internet address了,s_addr是什么意思呢?说实话我没猜出值得肯定的答案(根据下面网友的评论,其意思为source address,谢谢),也许就是socket address的意思吧,尽管跟更广义的sockaddr结构意思有所重复了。哎,这些都是历史原因,也许我是没有精力去考究了。
sockaddr和sockaddr_in在Linux中的实现
你可能还记得我之前说过,UNIX和Linux上的socket实现都是从BSD的socket实现演变过来的。事实上,socket这个词本来的意思,就是Berkeley Socket interface的简单说法。Linux上的socket与原本的socket的应该是完全兼容的,不过发展到今天,在代码实现上可能有些小的差别。我们就吹毛求疵的来看看这些区别在什么地方。
#include
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
//==============
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;
/* This macro is used to declare the initial common members
of the data types used for socket addresses, `struct sockaddr',
`struct sockaddr_in', `struct sockaddr_un', etc. */
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
可以看到,转了几次typedef,几次宏定义,实际效果是与标准socket一样的。
#include
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
//=================
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
同样的,看起来挺复杂,实际上与标准socket的定义是一样的。
头文件依赖关系
是包含在中的,是包含在中的,实际上我们在程序中往往就是:
值得知道的是,ARPA是 Advanced research project agency(美国国防部高级研究计划暑)的所写,ARPANET是当今互联网的前身,所以我们就可以想象,为什么inet.h会在arpa目录下了。
四。构造函数涉及的概念
话题回到"黑社会办公室"的例子,讲概念已经扯得比较远了,不过,这一节我们还得讲概念,不过好在有些程序的例子。如果大家不想翻回去看TcpServer类的原型,我这里直接给出这个头文件的完整源代码:
//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
我们已经解释了为什么listenSock和communicationSock的类型是int,以及sockaddr_in是什么结构,现在来写这个类的构造函数:
TcpServer::TcpServer(int listen_port)
{
if ( (listenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) 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)) >Application>>TCP/UDP>>IP>>OS(Driver, Kernel & Physical Address)
我们用socket重点描述的是协议,包括网络协议(IP)和传输协议(TCP/UDP)。
sockaddr重点描述的是地址,包括IP地址和TCP/UDP端口。
socket()函数
我们从TcpServer::TcpServer()函数可以看到,socket和sockaddr的产生是可以相互独立的。socket()的函数原型是:
#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(protocol family : internet),很多时候你会发现人们也经常在这里赋值为AF_INET,事实上,当前,AF_INET就是PF_INET的一个#define,但是,写成PF_INET从语义上会更加严谨。这也就是TCP/IP协议簇中的IP协议(Internet Protocol),网络层的协议。
后面两个参数定义传输层的协议。
第二个参数是传输层协议类型,我们教程里用到的宏,只有两个: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()。
动作与持续行为
本节最后的一个概念可以跟计算机无关。作为动词,有些可以描述动作,有些是描述一重持续的行为状态的(就如同一般动词和be动词一样)。扯到C++来说,我们可以把持续行为封装到函数内部,只留出动作的接口。事实上,构造函数中的bind()和listen()就是这种描述持续状态的行为函数。
五。创建监听套接字
前面一小节,我们已经写出了TcpServer的构造函数。这个函数的实际作用,就是创建了listen socket(监听嵌套字)。这一节,我们来具体分析这个创建的过程。
socket和sockaddr的创建是可以相互独立的
在函数中,我们首先通过socket()系统调用创建了listenSock,然后通过为结构体赋值的方法具体定义了服务器端的sockaddr。(memset()函数的作用是把某个内存段的空间设定为某值,这里是清零。)其他的概念已经在前一小节讲完了。这里需要补充的是说明宏定义INADDR_ANY。这里的意思是使用本机所有可用的IP地址。当然,如果你机器绑定了多个IP地址,你也可以指定使用哪一个。
数据流简易模型(SOCK_STREAM)
我们的例子以电话做的比喻,实际上,socket stream模型不完全类似电话,它至少有以下这些特点:
1、一种持续性的连接。这点跟电话是类似的,也可以想象成流动着液体的水管。一旦断开,这种流动就会中断。
2、数据包的发送实际上是非连续的。这个世界上有什么事物是真正的线性连续的?呵呵,扯远了,这貌似一个哲学问题。我们仅仅需要知道的是,一个数据包不可能是无限大的,所以,总是一个小数据包一个小数据包这样的发送的。这一点,又有点像邮包的传递。这些数据包到达与否,到达的先后次序本身是无法保证的,即是说,是IP协议无法保证的。但是stream形式的TCP协议,在IP之上,做了一定到达和到达顺序的保证。
3、传送管道实际上是非封闭的。要不干嘛叫"网络"-_-!!!。我们之所以能保证数据包的"定点"传送,完全是依靠每个数据包都自带了目的地址信息。
由此可见,虽然socket和sockaddr可以分别创建,并无依赖关系。但是在实际使用的时候,一个socket至少会绑定一个本机的sockaddr,没有自己的"地址信息",就不能接受到网络上的数据包(至少在TCP协议里面是这样的)。
socket与本机sockaddr的绑定
有时候绑定是系统的任务,特别是当你不需要知道自己的IP地址和所使用的端口号的时候。但是,我们现在是建立服务器,你必须告诉客户端你的连接信息:IP和Port。所以,我们需要指明IP和Port,然后进行绑定。
作为C++的程序员,也许你会觉得这个函数很不友好,它似乎更应该写成:
我们需要通过函数原型指明两点:
1、我们仅仅使用sockaddr结构的数据,但并不会对原有的数据进行修改;
2、我们使用的是完整的结构体,而不仅仅是这个结构体的指针。(很显然光用指针是无法说明结构体大小的)
幸运的是,在Linux的实现中,这个函数已经被写为:
看到亲切的const,我们就知道这个指针带入是没有"副作用"的。
监听:listen()
stream流模型形式上是一种"持续性"的连接,这就是要求信息的流动是"可来可去"的。也就是说,stream流的socket除了绑定本机的sockaddr,还应该拥有对方sockaddr的信息。在listen()中,这"对方的sockaddr"就可以不是某一个特定的sockaddr。实际上,listen socket的目的是准备被动的接受来自"所有"sockaddr的请求。所以,listen()反而就不能指定某个特定的sockaddr。
其中第二个参数是等待队列的限制,一般设置在5-20。Linux中实现为:
完成了这一步,回到我们的例子,就像是让你小弟在电话机前做好了接电话的准备工作。需要再次强调的是,这些行为仅仅是改变了socket的状态,实际上我想强调的是,为什么这些函数不会造成block(阻塞)的原因。(block的概念以后再解释)
六。创建"通讯"套接字
这里的"通讯"加上了引号,是因为实际上所有的socket都有通讯的功能,只是在我们的例子中,之前那个socket只负责listen,而这个socket负责接受信息并echo回去。
我们现看看这个函数:
bool TcpServer::isAccept()
{
unsigned int clntAddrLen = sizeof(clntAddr);
if ( (communicationSock = accept(listenSock, (sockaddr*)&clntAddr, &clntAddrLen)) socket
在我们的例子中,communicationSock实际上是用函数accept()创建的。
/* Await a connection on socket FD.
When a connection arrives, open a new socket to communicate with it,
set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
peer and *ADDR_LEN to the address's actual length, and return the
new socket's descriptor, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int accept (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()函数:
虽然我们这里不打算详细说明这个client端的函数,但是我们可以看出来,这个函数与之前我们介绍的bind()有几分相似,特别在Linux的实现中:
/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
For connectionless socket types, just set the default address to send to
and the only address from which to accept transmissions.
Return 0 on success, -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
connect() 也使用了const的sockaddr,只不过是远程电脑上的而非bind()的本机。
accept()在server端表面上是通过listen socket创建了新的socket,实际上,这种行为是在接受对方客户机程序中connect()函数的请求后发生的。综合起看,被创建的新socket实际上包含了listen socket的信息以及客户端connect()请求中所包含的信息--客户端的sockaddr地址。
新socket与sockaddr的关系
accept()创建的新socket(我们例子中的communicationSock,这里我们简单用newSock来带指)首先包含了listen socket的信息,所以,newSock具有本机sockaddr的信息;其次,因为它响应于client端connect()函数的请求,所以,它还包含了clinet端sockaddr的信息。
我们说过,stream流形式的TCP协议实际上是建立起一个"可来可去"的通道。用于listen的通道,远程机的目标地址是不确定的;但是newSock却是有指定的本机地址和远程机地址,所以,这个socket,才是我们真正用于TCP"通讯"的socket。
inet_ntoa()
对于这个函数,我们可以作为一种,将IP地址,由in_addr结构转换为可读的ASCII形式的固定用法。
七。接受与发送
现在,我们通过accept()创建了新的socket,也就是我们类中的数据成员communicationSock,现在,我们就可以通过这个socket进行通讯了。
TCP通讯模型
在介绍函数之前,我们应该了解一些事实。TCP的Server/Client模型类似这样:
ServApp--ServSock--Internet--ClntSock--ClntApp
当然,我们这里的socket指的就是用于"通讯"的socket。TCP的server端至少有两个socket,一个用于监听,一个用于通讯;TCP的server端可以只有一个socket,这个socket同时"插"在server的两个socket上。当然,插上listen socket的目的只是为了创建communication socket,创建完备后,listen是可以关闭的。但是,如果这样,其他的client就无法再连接上server了。
我们这个模型,是client的socket插在server的communication socket上的示意。这两个socket,都拥有完整的本地地址信息以及远程计算机地址信息,所以,这两个socket以及之间的网络实际上形成了一条形式上"封闭"的管道。数据包只要从一端进来,就能知道出去的目的地,反之亦然。这正是TCP协议,数据流形式抽象化以及实现。因为不再需要指明"出处"和"去向",对这样的socket(实际上是S/C上的socket对)的操作,就如同对本地文件描述符的操作一样。但是,尽管我们可以使用read()和write(),但是,为了完美的控制,我们最好使用recv()和send()。
recv()和send()
#include
/* Send N bytes of BUF to socket FD. Returns the number sent or -1.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t send (int __fd, __const void *__buf, size_t __n, int __flags);
/* Read N bytes into BUF from socket FD.
Returns the number read or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);
这两个函数的第一个参数是用于"通讯"的socket,第二个参数是发送或者接收数据的起始点指针,第三个参数是数据长度,第四个参数是控制符号(默认属性设置为0就可以了)。失败时候传回-1,否则传回实际发送或者接收数据的大小,返回0往往意味着连接断开了。
处理echo行为
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))
#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
//Filename: TcpServerClass.cpp
#include "TcpServerClass.hpp"
TcpServer::TcpServer(int listen_port)
{
if ( (listenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) 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))
#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;
}