Socket编程知识必学.docx
《Socket编程知识必学.docx》由会员分享,可在线阅读,更多相关《Socket编程知识必学.docx(49页珍藏版)》请在冰点文库上搜索。
Socket编程知识必学
整理自《Linux网络编程》
*/
端口号常识:
端口号被从1开始分配。
通常端口号超出255的部分被本地主机保留为私有用途。
1到255之间的号码被用于远程应用程序所请求的进程和网络服务。
每个网络通信循环地进出主计算机的TCP应用层。
它被两个所连接的号码唯一地识别。
这两个号码合起来叫做套接字.
组成套接字的这两个号码就是机器的IP地址和TCP软件所使用的端口号。
套接字的三种类型
流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)及原始套接字(RAW)。
流式套接字(SOCK_STREAM)
流式的套接字可以提供可靠的、面向连接的通讯流。
如果你通过流式套接字发送了顺序的数据:
“1”、“2”。
那么数据到达远程时候的顺序也是“1”、“2”。
Telnet应用程序、BBS服务、以及系统的远程登陆都是通过Telnet协议连接的。
Telnet就是一个流式连接。
你是否希望你在Telnet应用程序上输入的字符(或汉字)在到达远程应用程序的时候是以你输入的顺序到达的?
答案应该是肯定的吧。
还有WWW浏览器,它使用的HTTP协议也是通过流式套接字来获取网页的。
事实上,如果你Telnet到一个WebSite的80端口上,然后输入“GET网页路径名”然后按两下回车(或者是两下Ctrl+回车)然后你就得到了“网页路径名”所代表的网页!
数据报套接字(SOCK_DGRAM)
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。
如果你发送了一个数据报,它可能不会到达。
它可能会以不同的顺序到达。
如果它到达了,它包含的数据中可能存在错误。
数据报套接字也使用IP,但是它不使用TCP,它使用使用者数据报协议UDP(UserDatagramProtocol可以参考RFC768)
为什么说它们是“无连接”的呢?
因为它(UDP)不像流式套接字那样维护一个打开的连接,你只需要把数据打成一个包,把远程的IP贴上去,然后把这个包发送出去。
这个过程是不需要建立连接的。
UDP的应用例子有:
tftp,bootp等。
那么,数据包既然会丢失,怎样能保证程序能够正常工作呢?
事实上,每个使用UDP的程序都要有自己的对数据进行确认的协议。
比如,TFTP协议定义了对于每一个发送出去的数据包,远程在接受到之后都要回送一个数据包告诉本地程序:
“我已经拿到了!
”(一个“ACK”包)。
如果数据包发的送者在5秒内没有的得到回应,它就会重新发送这个数据包直到数据包接受者回送了“ACK”信号。
这些知识对编写一个使用UDP协议的程序员来说是非常必要的。
无连接服务器一般都是面向事务处理的,一个请求一个应答就完成了客户程序与服务程序之间的相互作用。
面向连接服务器处理的请求往往比较复杂,不是一来一去的请求应答所能解决的,而且往往是并发服务器
套接字工作过程如下:
服务器首先启动
通过调用socket()建立一个套接字,
然后调用bind()将该套接字和本地网络地址联系在一起,
再调用listen()使套接字做好侦听的准备,并规定它的请求队列的长度,
之后就调用accept()来接收连接。
客户在建立套接字
然后就可调用connect()和服务器建立连接。
客户机和服务器之间就可以通过调用read()和write()来发送和接收数据。
最后,待数据传送结束后,双方调用close()关闭套接字。
对流式套接字你所需要做的只是调用send()函数来发送数据。
而对于数据报套接字,你需要自己加个信息头,然后调用sendto()函数把数据发送出去
原始套接字
原始套接字主要用于一些协议的开发,可以进行比较底层的操作。
它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字
套接字结构
structsockaddr
这个结构用来存储套接字地址。
数据定义:
structsockaddr
{
unsignedshortsa_family; /*address族,AF_xxx*/
charsa_data[14]; /*14bytes的协议地址*/
};
sa_family 一般来说,都是“AF_INET”。
sa_data 包含了一些远程电脑的地址、端口和套接字的数目,它里面的数据是杂溶在一切的。
为了处理structsockaddr,程序员建立了另外一个相似的结构structsockaddr_in(“in”代表“Internet”):
structsockaddr_in
{
short int sin_family; /*Internet地址族*/
unsigned shortintsin_port; /*端口号*/
struct in_addr sin_addr; /*Internet地址*/
unsigned char sin_zero[8]; /*添0(和structsockaddr一样大小)*/
};
注意:
1)这个结构提供了方便的手段来访问socketaddress(structsockaddr)结构中的每一个元素。
2)sin_zero[8]是为了是两个结构在内存中具有相同的尺寸
要把sin_zero全部设成零值(使用bzero()或memset()函数)。
3)一个指向structsockaddr_in的指针可以声明指向一个sturctsockaddr的结构。
所以虽然socket()函数需要一个structaddr*,你也可以给他一个sockaddr_in*。
4)在struct sockaddr_in中,sin_family相当于在structsockaddr中的sa_family,需要设成“AF_INET”。
5)一定要保证sin_port和sin_addr必须是网络字节顺序(见下节)!
2.structin_addr (因特网地址(astructureforhistoricalreasons))
structin_addr
{
unsignedlongs_addr;
};
如果你声明了一个"ina"作为一个structsockaddr_in的结构,那么“ina.sin_addr.s_addr”就是4个字节的IP地址(按网络字节顺序排放)。
需要注意的是,即使你的系统仍然使用联合而不是结构来表示structin_addr,你仍然可以用上面的方法得到4个字节的IP地址(一些#defines帮了你的忙)
网络字节顺序
因为每一个机器内部对变量的字节存储顺序不同(有的系统是高位在前,底位在后,而有的系统是底位在前,高位在后),而网络传输的数据大家是一定要统一顺序的。
所以对与内部字节表示顺序和网络字节顺序不同的机器,就一定要对数据进行转换(比如IP地址的表示,端口号的表示)。
但是内部字节顺序和网络字节顺序相同的机器该怎么办呢?
是这样的:
它们也要调用转换函数,但是真正转换还是不转换是由系统函数自己来决定的。
有关的转化函数
我们通常使用的有两种数据类型:
短型(两个字节)和长型(四个字节)。
下面介绍的这些转换函数对于这两类的无符号整型变量都可以进行正确的转换。
如果你想将一个短型数据从主机字节顺序转换到网络字节顺序的话,有这样一个函数htons:
它是以“h”开头的(代表“主机”);
紧跟着它的是“to”,代表“转换到”;
然后是“n”代表“网络”;
最后是“s”,代表“短型数据”。
H-to-n-s,就是htons()函数(可以使用HosttoNetworkShort来助记)
你可以使用“n”,“h”,“to”,“s”,“l”的任意组合.当然,你要在可能的情况下进行组合。
比如,系统是没有stolh()函数的(ShorttoLongHost?
)。
下面给出套接字字节转换程序的列表:
htons()——“HosttoNetworkShort”主机字节顺序转换为网络字节顺序(对无符号短型进行操作4bytes)
htonl()——“HosttoNetworkLong” 主机字节顺序转换为网络字节顺序(对无符号长型进行操作8bytes)
ntohs()——“NetworktoHostShort“ 网络字节顺序转换为主机字节顺序(对无符号短型进行操作4bytes)
ntohl()——“NetworktoHostLong“ 网络字节顺序转换为主机字节顺序(对无符号长型进行操作8bytes)
在structsockaddr_in中的sin_addr和sin_port他们的字节顺序都是网络字节顺序,而sin_family却不是网络字节顺序的。
为什么呢?
这个是因为sin_addr和sin_port是从IP和UDP协议层取出来的数据,而在IP和UDP协议层,是直接和网络相关的,所以,它们必须使用网络字节顺序。
然而,sin_family域只是内核用来判断structsockaddr_in是存储的什么类型的数据,并且,sin_family永远也不会被发送到网络上,所以可以使用主机字节顺序来存储
socket()函数
/**
*
*取得套接字描述符!
(记得我们以前说过的吗?
它其实就是一个文件描述符)
*
*domain 需要被设置为“AF_INET”,就像上面的structsockaddr_in。
*type 参数告诉内核这个socket是什么类型,“SOCK_STREAM”或是“SOCK_DGRAM”。
*protocol通常为0
*
*return 如果发生错误,socket()函数返回–1。
全局变量errno将被设置为错误代码。
*
*/
#include
#include
intsocket(intdomain,inttype,intprotocol)
示例:
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("createsock");
return-1;
}
else
{
//printf("socketcreated...\n");
}
bind()函数
/**
*
*为套接字绑定一个端口号
*
*当你需要进行端口监听listen()操作,等待接受一个连入请求的时候,
*一般都需要经过这一步。
比如网络泥巴(MUD),Telneta.b.c.d4000
*
*如果你只是想进行连接一台服务器,也就是进行connect()操作的时候,这一步并不是必须的。
*
*
*sockfd 是由socket()函数返回的套接字描述符
*my_addr 是一个指向structsockaddr的指针,包含有关你的地址的信息:
名称、端口和IP地址。
*addrlen 可以设置为sizeof(structsockaddr)
*
*return 调用错误的时候,返回-1作为错误发生的标志。
errno的值为错误代码。
*
*/
#include
#include
intbind(intsockfd,structsockaddr*my_addr,intaddrlen);
示例:
#include
#include
#include
#define MYPORT4000
main()
{
intsockfd;
structsockaddr_inmy_addr;
sockfd =socket(AF_INET,SOCK_STREAM,0);/*在你自己的程序中要进行错误检查!
!
*/
my_addr.sin_family =AF_INET; /*主机字节顺序*/
my_addr.sin_port =htons(MYPORT); /*网络字节顺序,短整型*/
my_addr.sin_addr.s_addr=inet_addr("166.111.69.52");
bzero(&(my_addr.sin_zero),8); /*将整个结构剩余部分数据设为0*/
bind(sockfd,(structsockaddr*)&my_addr,sizeof(structsockaddr)); /*不要忘记在你自己的程序中加入判断bind错误的代码!
!
*/
注意:
my_addr.sin_port 是网络字节顺序,短整型
my_addr.sin_addr.s_addr 也是网络字节顺序。
最后,bind()可以在程序中自动获取你自己的IP地址和端口。
my_addr.sin_port=0; /*随机选择一个端口*/
my_addr.sin_addr.s_addr=INADDR_ANY;/*使用自己的地址*/
如上,通过设置my_addr.sin_port为0,bind()可以知道你要它帮你选择合适的端口;
通过设置my_addr.sin_addr.s_addr为INADDR_ANY,bind()知道你要它将s_addr填充为运行这个进程的机器的IP。
这一切都可以要求bind()来自动的帮助你完成。
如果你注意到了一些细节的话,你可能会发现我并没有将INADDR_ANY转换为网络字节顺序!
是这样的,INADDR_ANY的值为0,0就是0,无论用什么顺序排列位的顺序,它都是不变的。
有读者会想了,因为我用的INADDR_ANY是一个#define,那么如果将我的程序移植到另外一个系统,假如那里的INADDR_ANY是这样定义的:
#defineINADDR_ANY100,
那么我的程序不是就会不运行了吗?
那么下面这段代码就OK了:
my_addr.sin_port=htons(0); /*随机选择一个未用的端口*/
my_addr.sin_addr.s_addr=htonl(INADDR_ANY); /*使用自己的IP地址*/
现在我们已经是这么的严谨,对于任何数值的INADDR_ANY调用bind的时候就都不会有麻烦了。
另外一件必须指出的事情是:
当你调用bind()的时候,不要把端口数设置的过小!
小于1024的所有端口都是保留下来作为系统使用端口的,没有root权利无法使用。
你可以使用1024以上的任何端口,一直到65535:
你所可能使用的最大的端口号(当然,你还要保证你所希望使用的端口没有被其他程序所使用)。
最后注意有关bind()的是:
有时候你并不一定要调用bind()来建立网络连接。
比如你只是想连接到一个远程主机上面进行通讯,你并不在乎你究竟是用的自己机器上的哪个端口进行通讯(比如Telnet),那么你可以简单的直接调用connect()函数,connect()将自动寻找出本地机器上的一个未使用的端口,然后调用bind()来将其socket绑定到那个端口上。
connect()函数
/*
*
*sockfd 套接字文件描述符,由socket()函数返回的
*serv_addr 是一个存储远程计算机的IP地址和端口信息的结构
*addrlen 应该是sizeof(structsockaddr)
*
*return 如果发生了错误(比如无法连接到远程主机,或是远程主机的指定端口无法进行连接等)它将会返回错误值-1
* 全局变量errno将会存储错误代码
*
*
*/
#include
#include
intconnect(intsockfd,structsockaddr*serv_addr,intaddrlen);
示例:
#include
#include
#include
#defineDEST_IP“166.111.69.52”
#defineDEST_PORT23
main()
{
intsockfd;
/*将用来存储远程信息*/
structsockaddr_indest_addr;
/*注意在你自己的程序中进行错误检查!
!
*/
sockfd=socket(AF_INET,SOCK_STREAM,0);
/*主机字节顺序*/
dest_addr.sin_family=AF_INET;
/*网络字节顺序,短整型*/
dest_addr.sin_port=htons(DEST_PORT(;
dest_addr.sin_addr.s_addr=inet_addr(DEST_IP);
/*将剩下的结构中的空间置0*/
bzero(&(dest_addr.sin_zero),8);
/*不要忘记在你的代码中对connect()进行错误检查!
!
*/
connect(sockfd,(structsockaddr*)&dest_addr,sizeof(structsockaddr));
注意我们没有调用bind()函数。
基本上,我们并不在乎我们本地用什么端口来通讯,是不是?
我们在乎的是我们连到哪台主机上的哪个端口上。
Linux内核自动为我们选择了一个没有被使用的本地端口。
listen()函数
/*
*
*等待别人连接,进行系统侦听请求
*当有人连接你的时候,你有两步需要做:
* 通过listen()函数等待连接请求
* 然后使用accept()函数来处理
*
*那么我们需要指定本地端口了,因为我们是等待别人的连接。
所以,在listen()函数调用之前,我们需要使用bind()函数来指定使用本地的哪一个端口数值
*如果你想在一个端口上接受外来的连接请求的话,那么函数的调用顺序为:
* socket();
* bind();
* listen();
*
*sockfd 是一个套接字描述符,由socket()系统调用获得
*backlog 是未经过处理的连接请求队列可以容纳的最大数目(每一个连入请求都要进入一个连入请求队列,等待listen的程序调用accept()函数来接受这个连接。
当系统还没有调用accept()函数的时候,如果有很多连接,那么本地能够等待的最大数目就是backlog的数值。
你可以将其设成5到10之间的数值(推荐))
*
*
*return 错误返回-1,并设置全局错误代码变量errno
*/
#include
intlisten(intsockfd,intbacklog);
accept()函数
/*
*当调用它的时候,大致过程是下面这样的:
* 有人从很远很远的地方尝试调用connect()来连接你的机器上的某个端口(当然是你已经在listen()的)
* 他的连接将被listen加入等待队列等待accept()函数的调用
* 你调用accept()函数,告诉他你准备连接
*
*sockfd 是正在listen()的一个套接字描述符
*addr 一般是一个指向structsockaddr_in结构的指针;里面存储着远程连接过来的计算机的信息(比如远程计算机的IP地址和端口)
*addrlen 是一个本地的整型数值,在它的地址传给accept()前它的值应该是sizeof(structsockaddr_in);accept()不会在addr中存储多余addrlenbytes大小的数据。
如果accept()函数在addr中存储的数据量不足addrlen,则accept()函数会改变addrlen的值来反应这个情况。
*
*
*return accept()函数将回返回一个新的套接字描述符,这个描述符就代表了这个连接
* 这时候你有了两个套接字描述符:
* 返回给你的那个就是和远程计算机的连接,这时候你所得到的那个新的套接字描述符就可以进行send()操作和recv()操作了。
* 而第一个套接字描述符仍然在你的机器上原来的那个端口上listen()
*
* -1来表明调用失败,同时全局变量errno将会存储错误代码
*/
#include
intaccept(intsockfd,void*addr,int*addrlen);
示例:
#include
#include