Linuxsocket编程入门.docx
- 文档编号:8510173
- 上传时间:2023-01-31
- 格式:DOCX
- 页数:16
- 大小:30.05KB
Linuxsocket编程入门.docx
《Linuxsocket编程入门.docx》由会员分享,可在线阅读,更多相关《Linuxsocket编程入门.docx(16页珍藏版)》请在冰豆网上搜索。
Linuxsocket编程入门
Linuxsocket编程入门
(一)TCPserver端:
1、建模
作者:
龙飞
绝大部分关于socket编程的教程总是从socket的概念开始讲起的。
要知道,socket的初衷是个庞大的体系,TCP/IP只是这个庞大体系下一个很小的子集,而我们真正能用上的更是这个子集中的一小部分:
运输层(Host-to-HostTransportLayer)的TCP和UDP协议,以及使用这两个协议进行应用层(ApplicationLayer)的开发。
即使是socket的核心部分,网络层(InternetLayer)的IP协议,在编程的时候我们也很少会感觉到它的存在——因为已经被封装好了,我们唯一需要做的事情就是传入一个宏。
第一节我想介绍的概念就这么多,当然,既然我们已经说了3个层了,我想最好还是把最后一个层也说出来,即所谓链路层(NetworkAccessLayer),它包括了物理硬件和驱动程序。
这四个层从底到高的顺序是:
链路层--网络层--运输层--应用层。
好,说实话我们现在并不清楚所谓TCP到底是什么东东,不过我们知道这东东名气很大。
或许你早就知道,另外一个声名狼藉建立在TCP协议基础上的应用程序,它曾经几乎是统治了一个时代,即使是今天,我们依然无法消除他的影响力的——恩,是的,就是telnet。
在这个教程中,我使用的环境是DebianGNU/Linux4.0etch。
传说中的stable-_-!
!
!
,恩,我是很保守的人。
如果你不是自己DIY出来的系统,相信默认安装里面就应该有telnet(/usr/bin/telnet,要是没装就自己aptitudeinstall吧)。
telnet可以与所有遵循TCP协议的服务器端进行通讯。
通常,socket编程总是Client/Server形式的,因为有了telnet,我们可以先不考虑client的程序,我们先写一个支持TCP协议的server端,然后用telnet作为client验证我们的程序就好了。
server端的功能,我们也考虑一种最简单的反馈形式:
echo。
就如同你在终端输入echo'HelloWorld',回车后shell就会给你返回HelloWorld一样,我们的第一个TCPserver就用以实现这个功能。
什么样的模型适合描述这样的一种server呢?
我相信,一个很2的例子会有助于我们记忆TCPserver端的基本流程。
想象你自己是个小大佬,坐办公室(什么样的黑社会做办公室啊?
可能是讨债公司吧^^)你很土,只有一个小弟帮你接电话(因为你自己的号码是不敢对外公开的)。
一次通讯的流程大概应该是这样的:
小弟那里的总机电话响了;小弟接起电话;对方说是你女朋友A妹;小弟转达说,“老大,你马子电话”;你说,接过来;小弟把电话接给你;你和你女朋友聊天半小时;挂电话。
我们来分析一下整个过程中的元素。
先分析成员数据(请注意,这里开始用C++术语了):
你小弟(listenSock),你需要他来监听(listen,这是socket编程中的术语)电话;你自己(communicationSock),实际上打电话进行交流的是你自己;你的电话号码(servAddr),否则你女朋友怎么能找到你?
你女朋友的电话号码(clntAddr),这个比喻有点牵强,因为事实上你接起电话,不需要知道对方的号码也可以通话(虽然事实上你应该是知道的,你不会取消了来电显示功能吧^^),但是,难道你是只接女朋友电话从来不打过去的牛人吗?
这个过程中的行为(成员函数):
你小弟接电话并转接给你(isAccept());你自己的通话(handleEcho())(这个行为确实比较土,只会乌鸦学舌的echo,呵呵)。
简单的说,就是这些了。
根据这个模型,我们可以很容易写出实现我们需要的echo功能的TCPserver的类:
class TcpServer
{
private:
int listenSock;//小弟
int communicationSock;//大哥
sockaddr_in servAddr;//自己的电话
sockaddr_in clntAddr;//对方的电话
public:
TcpServer(intlisten_port);
bool isAccept();//小弟接起
void handleEcho();//大哥
};
这里面有些简写,比如,sock实际上就是socket,addr就是address。
serv和clnt我想你一定能猜到是server和client吧。
还有一个socket中的结构体sockaddr_in,实际上就是这个意思:
socketaddressinternet(网络嵌套字地址),具体解说,请看下回分解。
Linuxsocket编程入门
(一)TCPserver端:
2、socket与文件描述符
作者:
龙飞
UNIX中的一切事物都是文件(everythinginUnixisafile!
)
当我在这篇教程中提到UNIX的时候,其意思专指符合UNIX标准的所谓“正统”UNIX的衍生系统(其实我就用来带指那些买了最初UNIX源代码的商业系统)操作系统和类似Linux,BSD这些类UNIX系统。
如果某些要点是Linux特有的,或者因为本人孤陋寡闻暂时搞不清楚是Linux特有的还是UNIX通用的,我就会指明是Linux,甚至其发行版(我本人在写这篇教程的时候是以DebianGNU/Linux4.0etch为测试平台的)。
我们学习UNIX的时候,恐怕听到的第一句话就是这句:
UNIX中一切都是文件。
这是UNIX的基本理念之一,也是一句很好的概括。
比如,很多UNIX老鸟会举出个例子来,“你看,/dev/hdc是个文件,它实际上也是我的光盘……”UNIX中的文件可以是:
网络连接(networkconnection),输入输出(FIFO),管道(apipe),终端(terminal),硬盘上的实际文件,或者其它任何东东。
文件与文件描述符(file&filedescriptor)
你可能对上一章中建模类中的int还记忆犹新。
我们用int在描述socket,实际上,所有的文件描述符都是int,没错,用的是一个整数类型。
如果你觉得这样让你很难接受,那么恭喜你,你跟我一样,也许是深中C++面向对象思想的毒了^^。
因为是int,所以文件描述符不可能是C++概念中的对象,因为int无法发出行为,但是,这并不代表也不能接受一个动作哈。
PASCAL之父在批判面向对象思想教条的时候,曾经生动的举了个例子,“在OOP的概念中,绝对不应该接受a+b这种表达的,OOP对这个问题的表达应该是a.add(b)”。
fd(filedescriptor)可以作为接受动作的对象,但是本身却无法发出动作,这就如同一个只能做宾语不能做主语的名词,是个不完整的对象。
但是,请别忘了Linux和socket本身是C语言的产物,我们必须接受在面向过程时代下的产物,正视历史——当然,这与我们自己再进行OOP的封装并不矛盾。
我们应该记住3个已经打开的fd,0:
标准输入(STDIN_FILENO);1:
标准输出(STDOUT_FILENO);2:
标准错误(STDERR_FILENO)。
(以上宏定义在
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的类型是int?
”-_-!
!
!
Linuxsocket编程入门
(一)TCPserver端:
3、sockaddr与sockaddr_in
作者:
龙飞
收件人地址
一家化妆品公司将一批新产品的样品,准备发给某学校某个班的女生们免费试用。
通常情况下,这件邮包的地址上可以这么写:
收件人:
全体女生。
地址:
A省B市C学校,X级Y班。
但是,如果在描述地址的时候这样写呢:
收件人:
全体女生。
地址:
请打电话xxxxxxxx,找他们学校一个叫Lucy的女生,然后把东西送到她的班上。
这种文字是相当的诡异啊-_-!
!
!
,但是并不等于就没有表述清楚邮包的去向和地址。
事实上邮局看到这样的地址一定会发飙的,然而对于电脑,如果你的地址描述形式是他可以接受和执行的,他就会老老实实的按你的要求去做……
所以,如何描述地址不是问题的关键,关键在于这样的表述是不是能够表述清楚一个地址。
一种更加通用的表达形式可能是这样的:
收件人:
全体女生。
地址:
<一种地址描述方式>
事实上,在socket的通用address描述结构sockaddr中正是用这样的方式来进行地址描述的:
struct sockaddr
{
unsigned short sa_family;
char sa_data[14];
};
这是一个16字节大小的结构(2+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被定义成了一个结构,这个结构实际上就是:
struct in_addr
{
unsigned long s_addr;
};
in_addr显然是internetaddress了,s_addr是什么意思呢?
说实话我没猜出值得肯定的答案,也许就是socketaddress的意思吧,尽管跟更广义的sockaddr结构意思有所重复了。
哎,这些都是历史原因,也许我是没有精力去考究了。
sockaddr和sockaddr_in在Linux中的实现
你可能还记得我之前说过,UNIX和Linux上的socket实现都是从BSD的socket实现演变过来的。
事实上,socket这个词本来的意思,就是BerkeleySocketinterface的简单说法。
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的定义是一样的。
头文件依赖关系
#include
#include
值得知道的是,ARPA是Advancedresearchprojectagency(美国国防部高级研究计划暑)的所写,ARPANET是当今互联网的前身,所以我们就可以想象,为什么inet.h会在arpa目录下了。
Linuxsocket编程入门
(一)TCPserver端:
4、构造函数涉及的概念
作者:
龙飞
话题回到“黑社会办公室”的例子,讲概念已经扯得比较远了,不过,这一节我们还得讲概念,不过好在有些程序的例子。
如果大家不想翻回去看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)) < 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";
}
}
好,先看看程序培养一下感觉,我们还得说概念。
数据封装(DataEncapsutation)
我们前面说到了网络分层:
链路——网络——传输——应用。
数据从应用程序里诞生,传送到互联网上每一层都会进行一次封装:
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()。
动作与持续行为
本节最后的一个概念可以跟计算机无关。
作为动词,有些可以描述动
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Linuxsocket 编程 入门