现在的位置: 首页 > 综合 > 正文

socket简易入门手册

2013年09月14日 ⁄ 综合 ⁄ 共 9103字 ⁄ 字号 评论关闭

 


介绍

当你进入 UNIX 的神秘世界后,立刻会发现越来越多的东西难以理解。对于大多数人来说,BSD socket 的概念就是其中一个。这是一个很短的教程来解释他们是什么、他们如何工作并给出一些简单的代码来解释如何使用他们。


类比 (什么是 socket ?)

socket
是进行程序间通讯(IPC)的 BSD 方法。这意味着 socket 用来让一个进程和其他的进程互通信息,就象我们用电话来和其他的人交流一样。

用电话来比喻是很恰当的,我们在后面将一直用电话这个概念来描叙 socket 。


装上你的新电话(怎样侦听?)

一个人要能够收到别人打给他的电话,首先他要装上一门电话。同样,你必须先建立 socket 以侦听线路。这个过程包含几个步骤。首先,你要建立一个新的 socket,就象先装上电话一样。socket()
命令就完成这个工作。

因为 sockets 有几种类型,你要注明你要建立什么类型的。你要做一个选择是 socket 的地址格式。如同电话有音频和脉冲两种形式一样,socket 有两个最重要的选项是 AF_UNIX
IAF_INET
AF_UNIX
就象 UNIX 路径名一样识别 sockets。这种形式对于在同一台机器上的 IPC 很有用。而 AF_INET
使用象 192.9.200.10 这样被点号隔开的四个十进制数字的地址格式。除了机器地址以外,还可以利用端口
号来允许每台机器上的多个 AF_INET
socket。我们这里将着重于 AF_INET
方式,因为他很有用并广泛使用。

另外一个你必须提供的参数是 socket 的类型。两个重要的类型是 SOCK_STREAM
SOCK_DGRAM

SOCK_STREAM
表明数据象字符流一样通过 socket 。而 SOCK_DGRAM
则表明数据将是数据报(datagrams
)的形式。我们将讲解 SOCK_STREAM
sockets,他很常见并易于使用。

在建立 socket 后,我们就要提供 socket 侦听的地址了。就象你还要个电话号码来接电话一样。bind()
函数来处理这件事情。

SOCK_STREAM sockets 让连接请求形成一个队列。如果你忙于处理一个连接,别的连接请求将一直等待到该连接
处理完毕。listen()
函数用来设置最大不被拒绝的请求数(一般为5个)。
一般最好不要使用 listen()
函数。

下面的代码说明如何利用 socket()

bind()
listen()
函数建立连接并可以接受数据。

 

/* code to establish a socket; originally from bzs@bu-cs.bu.edu
*/

int establish(unsigned short portnum)
{ char myname[MAXHOSTNAME+1];
int s;
struct sockaddr_in sa;
struct hostent *hp;

memset(&sa, 0, sizeof(struct sockaddr_in)); /* clear our address */
gethostname(myname, MAXHOSTNAME); /* who are we? */
hp= gethostbyname(myname); /* get our address info */
if (hp == NULL) /* we don't exist !? */
return(-1);
sa.sin_family= hp->h_addrtype; /* this is our host address */
sa.sin_port= htons(portnum); /* this is our port number */
if ((s= socket(AF_INET, SOCK_STREAM, 0)) < 0) /* create socket */
return(-1);
if (bind(s,&sa,sizeof(struct sockaddr_in)) < 0) {
close(s);
return(-1); /* bind address to socket */
}
listen(s, 3); /* max # of queued connects */
return(s);
}

在建立完 socket 后,你要等待对该 socket 的调用了。accept()
函数为此目的而来。调用 accept()
如同在电话铃响后提起电话一样。Accept()
返回一个新的连接到调用方的 socket 。

下面的代码演示使用是个演示。

 

/* wait for a connection to occur on a socket created with establish()
*/
int get_connection(int s)
{ int t; /* socket of connection */

if ((t = accept(s,NULL,NULL)) < 0) /* accept connection if there is one */
return(-1);
return(t);
}

和电话不同的是,在你处理先前的连接的时候,你还可以接受调用。为此,一般用 fork 来处理每个连接。下面的代码演示如何使用 establish()
get_connection()
来处理多个连接。

 

#include <errno.h> /* obligatory includes */
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <netdb.h>

#define PORTNUM 50000 /* random port number, we need something */

void fireman(void);
void do_something(int);

main()
{ int s, t;

if ((s= establish(PORTNUM)) < 0) { /* plug in the phone */
perror("establish");
exit(1);
}

signal(SIGCHLD, fireman); /* this eliminates zombies */

for (;;) { /* loop for phone calls */
if ((t= get_connection(s)) < 0) { /* get a connection */
if (errno == EINTR) /* EINTR might happen on accept(), */
continue; /* try again */
perror("accept"); /* bad */
exit(1);
}
switch(fork()) { /* try to handle connection */
case -1 : /* bad news. scream and die */
perror("fork");
close(s);
close(t);
exit(1);
case 0 : /* we're the child, do something */
close(s);
do_something(t);
exit(0);
default : /* we're the parent so look for */
close(t); /* another connection */
continue;
}
}
}

/* as children die we should get catch their returns or else we get
* zombies, A Bad Thing. fireman() catches falling children.
*/
void fireman(void)
{
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}

/* this is the function that plays with the socket. it will be called
* after getting a connection.
*/
void do_something(int s)
{
/* do your thing with the socket here
:
:
*/
}


拨号 (如何调用 socket)

现在你应该知道如何建立 socket 来接受调用了。那么如何调用呢?和电话一样,你要先有个电话。用 socket()
函数来完成这件事情,就象建立侦听的 socket 一样。

在给 socket 地址后,你可以用 connect()
函数来连接侦听的 socket 了。下面是一段代码。

 

int call_socket(char *hostname, unsigned short portnum)
{ struct sockaddr_in sa;
struct hostent *hp;
int a, s;

if ((hp= gethostbyname(hostname)) == NULL) { /* do we know the host's */
errno= ECONNREFUSED; /* address? */
return(-1); /* no */
}

memset(&sa,0,sizeof(sa));
memcpy((char *)&sa.sin_addr,hp->h_addr,hp->h_length); /* set address */
sa.sin_family= hp->h_addrtype;
sa.sin_port= htons((u_short)portnum);

if ((s= socket(hp->h_addrtype,SOCK_STREAM,0)) < 0) /* get socket */
return(-1);
if (connect(s,&sa,sizeof sa) < 0) { /* connect */
close(s);
return(-1);
}
return(s);
}

这个函数返回一个可以流过数据的 socket 。


谈话(如何通过 sockets 交谈)

好了,你在要传输数据的双方建立连接了,现在该传输数据了。read()
write()
函数来处理吧。除了在 socket 读写和文件读写中的一个区别外,和处理一般的文件一样。区别是你一般不能得到你所要
的数目的数据。所以你要一直循环到你需要的数据的到来。一个简单的例子:将一定的数据读到缓存。

 

int read_data(int s, /* connected socket */
char *buf, /* pointer to the buffer */
int n /* number of characters (bytes) we want */
)
{ int bcount; /* counts bytes read */
int br; /* bytes read this pass */

bcount= 0;
br= 0;
while (bcount < n) { /* loop until full buffer */
if ((br= read(s,buf,n-bcount)) > 0) {
bcount += br; /* increment byte counter */
buf += br; /* move buffer ptr for next read */
}
else if (br < 0) /* signal an error to the caller */
return(-1);
}
return(bcount);
}

相同的函数也可以写数据,留给我们的读者吧。


挂起(结束)

和你通过电话和某人交谈后一样,你要在 socket 间关闭连接。一般 close()
函数用来关闭每边的 socket 连接。如果一边的已经关闭,而另外一边却在向他写数据,则返回一个错误代码。


世界语(交流的语言很重要)

现在你可以在机器间联络了,可是要小心你所说的话。许多机器有自己的方言,如 ASCII 和 EBCDIC。更常见的问题是字节顺序问题。除非你一直传输的都是文本,否则你一定要注意这个问题。幸运的是,人们找出了解决的办法。

在很久以前,人们争论哪种顺序更“正确”。现在必要时有相应的函数来转换。
其中有 htons()
ntohs()
htonl()
ntohl()
。在传输一个整型数据前,先转换一下。

 

i= htonl(i);
write_data(s, &i, sizeof(i));

在读数据后,再变回来。

 

read_data(s, &i, sizeof(i));
i= ntohl(i);

如果你一直坚持这个习惯,你将比别人少出错的机会。


未来在你的掌握了(下一步?)

就用我们刚才讨论的东西,你就可以写自己的通讯程序了。和对待所有的新生事
物一样, 最好还是看看别人已经做了些什么。这里有许多关于 BSD socket 的东西可以参考。

请注意,例子中没有错误检查,这在“真实”的程序中是很重要的。你应该对此充分重视。

 

 

 

socket的英文原义是“孔”或“插座”。在这里作为4BDS
UNIX的进程通信机制,取后一种意思。socket非常类似于电话插座。以一个国家级电话网为例。电话的通话双方相当于相互通信的2个进程,区号是它的
网络地址;区内一个单位的交换机相当于一台主机,主机分配给每个用户的局内号码相当于socket号。任何用户在通话之前,首先要占有一部电话机,相当于
申请一个socket;同时要知道对方的号码,相当于对方有一个固定的socket。然后向对方拨号呼叫,相当于发出连接请求(假如对方不在同一区内,还
要拨对方区号,相当于给出网络地址)。对方假如在场并空闲(相当于通信的另一主机开机且可以接受连接请求),拿起电话话筒,双方就可以正式通话,相当于连
接成功。双方通话的过程,是一方向电话机发出信号和对方从电话机接收信号的过程,相当于向socket发送数据和从socket接收数据。通话结束后,一
方挂起电话机相当于关闭socket,撤消连接。


  在电话系统中,一般用户只能感受到本地电话机和对方电话号码的存在,建立通话的过程,话音传输
的过程以及整个电话系统的技术细节对他都是透明的,这也与socket机制非常相似。socket利用网间网通信设施实现进程通信,但它对通信设施的细节
毫不关心,只要通信设施能提供足够的通信能力,它就满足了。
  至此,我们对socket进行了直观的描述。抽象出来,socket实质上提供了进程通信的端
点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之前,双方必须各自拥有一台电话机一样。在网间网内
部,每一个socket用一个半相关描述:
  (协议,本地地址,本地端口)
  一个完整的socket有一个本地唯一的socket号,由操作系统分配。
  最重要的是,socket
是面向客户/服务器模型而设计的,针对客户和服务器程序提供不同的socket 系统调用。客户随机申请一个socket
(相当于一个想打电话的人可以在任何一台入网电话上拨号呼叫),系统为之分配一个socket号;服务器拥有全局公认的 socket
,任何客户都可以向它发出连接请求和信息请求(相当于一个被呼叫的电话拥有一个呼叫方知道的电话号码)。
  socket利用客户/服务器模式巧妙地解决了进程之间建立通信连接的问题。服务器
socket 半相关为全局所公认非常重要。读者不妨考虑一下,两个完全随机的用户进程之间如何建立通信?假如通信双方没有任何一方的socket
固定,就好比打电话的双方彼此不知道对方的电话号码,要通话是不可能的。

 

Socket 接口是访问 Internet 使用得最广泛的方法。
如果你有一台刚配好TCP/IP协议的主机,其IP地址是202.120.127.201, 此时在另一台主机或同一台主机上执行ftp
202.120.127.201,显然无法建立连接。因"202.120.127.201" 这台主机没有运行FTP服务软件。同样,
在另一台或同一台主机上运行浏览软件
如Netscape,输入"http://202.120.127.201",也无法建立连接。现在,如果在这台主机上运行一个FTP服务软件(该软件将
打开一个Socket, 并将其绑定到21端口),再在这台主机上运行一个Web
服务软件(该软件将打开另一个Socket,并将其绑定到80端口)。这样,在另一台主机或同一台主机上执行ftp
202.120.127.201,FTP客户软件将通过21端口来呼叫主机上由FTP
服务软件提供的Socket,与其建立连接并对话。而在netscape中输入"http://202.120.127.201"时,将通过80端口来呼
叫主机上由Web服务软件提供的Socket,与其建 立连接并对话。

  在Internet上有很多这样的主机,这些主机一般运行了多个服务软件,同时提供几种服务。
每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原意那样,象一个多孔插座。一台主机犹如布满
各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。
客户软件将插头插到不同编号的插座,就可以得到不同的服务。

 

******************************************************************************************************************************

要写网络程序就必须用Socket,这是程序员都知
道的。而且,面试的时候,我们也会问对方会不会Socket编程?一般来说,很多人都会说,Socket编程基本就是listen,accept以及
send,write等几个基本的操作。是的,就跟常见的文件操作一样,只要写过就一定知道。

 

对于网络编程,我们也言必称TCP/IP,似乎其它
网络协议已经不存在了。对于TCP/IP,我们还知道TCP和UDP,前者可以保证数据的正确和可靠性,后者则允许数据丢失。最后,我们还知道,在建立连
接前,必须知道对方的IP地址和端口号。除此,普通的程序员就不会知道太多了,很多时候这些知识已经够用了。最多,写服务程序的时候,会使用多线程来处理
并发访问。

 

我们还知道如下几个事实:

1。一个指定的端口号不能被多个程序共用。比如,如果IIS占用了80端口,那么Apache就不能也用80端口了。

2。很多防火墙只允许特定目标端口的数据包通过。

3。服务程序在listen某个端口并accept某个连接请求后,会生成一个新的socket来对该请求进行处理。

 

于是,一个困惑了我很久的问题就产生了。如果一个
socket创建后并与80端口绑定后,是否就意味着该socket占用了80端口呢?如果是这样的,那么当其accept一个请求后,生成的新的
socket到底使用的是什么端口呢(我一直以为系统会默认给其分配一个空闲的端口号)?如果是一个空闲的端口,那一定不是80端口了,于是以后的TCP
数据包的目标端口就不是80了--防火墙一定会组织其通过的!实际上,我们可以看到,防火墙并没有阻止这样的连接,而且这是最常见的连接请求和处理方式。
我的不解就是,为什么防火墙没有阻止这样的连接?它是如何判定那条连接是因为connet80端口而生成的?是不是TCP数据包里有什么特别的标志?或者
防火墙记住了什么东西?

 

后来,我又仔细研读了TCP/IP的协议栈的原理,
对很多概念有了更深刻的认识。比如,在TCP和UDP同属于传输层,共同架设在IP层(网络层)之上。而IP层主要负责的是在节点之间(End to
End)的数据包传送,这里的节点是一台网络设备,比如计算机。因为IP层只负责把数据送到节点,而不能区分上面的不同应用,所以TCP和UDP协议在其
基础上加入了端口的信息,端口于是标识的是一个节点上的一个应用。除了增加端口信息,UPD协议基本就没有对IP层的数据进行任何的处理了。而TCP协议
还加入了更加复杂的传输控制,比如滑动的数据发送窗口(Slice
Window),以及接收确认和重发机制,以达到数据的可靠传送。不管应用层看到的是怎样一个稳定的TCP数据流,下面传送的都是一个个的IP数据包,需
要由TCP协议来进行数据重组。

 

所以,我有理由怀疑,防火墙并没有足够的信息判断TCP数据包的更多信息,除了IP地址和端口号。而且,我们也看到,所谓的端口,是为了区分不同的应用的,以在不同的IP包来到的时候能够正确转发。

 

TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。就像操作系统会提供标准的编程接口,比如Win32编程接口一样,TCP/IP也必须对外提供编程接口,这就是Socket编程接口--原来是这么回事啊!

 

在Socket编程接口里,设计者提出了一个很重要
的概念,那就是socket。这个socket跟文件句柄很相似,实际上在BSD系统里就是跟文件句柄一样存放在一样的进程句柄表里。这个socket其
实是一个序号,表示其在句柄表中的位置。这一点,我们已经见过很多了,比如文件句柄,窗口句柄等等。这些句柄,其实是代表了系统中的某些特定的对象,用于
在各种函数中作为参数传入,以对特定的对象进行操作--这其实是C语言的问题,在C++语言里,这个句柄其实就是this指针,实际就是对象指针啦。

 

现在我们知道,socket跟TCP/IP并没有必
然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以,socket的出现只是可以更方便的使用TCP/IP协议栈而已,其
对TCP/IP进行了抽象,形成了几个最基本的函数接口。比如create,listen,accept,connect,read和write等等。

 

现在我们明白,如果一个程序创建了一个
socket,并让其监听80端口,其实是向TCP/IP协议栈声明了其对80端口的占有。以后,所有目标是80端口的TCP数据包都会转发给该程序(这
里的程序,因为使用的是Socket编程接口,所以首先由Socket层来处理)。所谓accept函数,其实抽象的是TCP的连接建立过程。
accept函数返回的新socket其实指代的是本次创建的连接,而一个连接是包括两部分信息的,一个是源IP和源端口,另一个是宿IP和宿端口。所
以,accept可以产生多个不同的socket,而这些socket里包含的宿IP和宿端口是不变的,变化的只是源IP和源端口。这样的话,这些
socket宿端口就可以都是80,而Socket层还是能根据源/宿对来准确地分辨出IP包和socket的归属关系,从而完成对TCP/IP协议的操
作封装!而同时,放火墙的对IP包的处理规则也是清晰明了,不存在前面设想的种种复杂的情形。

 

明白socket只是对TCP/IP协议栈操作的抽象,而不是简单的映射关系,这很重要!

 


抱歉!评论已关闭.