Berkeley套接字的一些基本知识

一、首先看一下Berkeley Socket实现TCP和UDP协议的流程
1.面向连接的TCP
[align=center][img]http://dl.iteye.com/upload/attachment/160430/d016d948-ff9b-3c51-8dd5-fbf138489b27.jpg[/img][/align]

2.无连接的UDP
[align=center][img]http://dl.iteye.com/upload/attachment/160432/42a4fbe5-98f9-3994-8b9c-0edc7ed64beb.jpg[/img][/align]

二、Berkeley套接字的一些基本知识
1.基本结构
1.1、struct sockaddr
struct sockaddr{
unsigned short sa_family;
char sa_data[14];
};

sa_family 一般来说,都是“AFINET”。
sa_data 包含了一些远程电脑的地址、端口和套接字的数目,它里面的数据是杂溶在一起的。为了处理 struct sockaddr, 程序员建立了另外一个相似的结构struct sockaddr_in,struct sockaddr_in (“in” 代表 “Internet”)。

struct sockaddr_in {
short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* Internet地址 */
unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小)*/
};


1.2、struct in_addr
struct in_addr{
unsigned long s_addr;
};


2.基本转换函数
2.1.网络字节顺序
……
2.2.有关的转换函数
[list]
[*]htons()——“Host to Network Short” 主机字节顺序转换为网络字节顺序(对无符号短型进行操作4 bytes)
[*]htonl()——“Host to Network Long”   主机字节顺序转换为网络字节顺序(对无符号长型进行操作 8 bytes)
[*]ntohs()——“ Network to Host Short” 网络字节顺序转换为主机字节顺序(对无符号短型进行操作 4 bytes)
[*]ntohl()——“ Network to Host Long ”  网络字节顺序转换为主机字节顺序(对无符号长型进行操作 8 bytes)
[/list]

在 struct sockaddr_in 中的 sin_addr 和 sin_port 他们的字节顺序都是网络字节顺序,而sin_family

却不是网络字节顺序的。为什么呢?
这个是因为 sin_addr 和sin_port 是从IP 和 UDP 协议层取出来的数据,而在IP和 UDP协议层,是直接和网络相关的,所以,它们必须使用网络字节顺序。然而, sin_family 域只是内核用来判断 struct sockaddr_in 是存储的什么类型的数据,并且,sin_family 永远也不会被发送到网络上,所以可以使用主机字节顺序来存储。

2.3.IP 地址转换
很幸运, Linux 系统提供和很多用于转换 IP 地址的函数,使你不必自己再写出一段费力不讨好的子程序来吃力的变换 IP。首先,让我假设你有一个 struct sockaddr_in ina,并且你的 IP 是 166.111.69.52 ,你想把你的 IP 存储到 ina 中。你可以使用的函数:inet_addr() ,它能够把一个用数字和点表示 IP地址的字符串转换成一个无符号长整型。
你可以像下面这样使用它:
ina.sin_addr.s_addr = inet_addr(“ 166.111.69.52” );
注意:inet_addr() 返回的地址已经是网络字节顺序了,你没有必要再去调用 htonl()函数。
[list]
[*]inet_ntoa() 使用struct in_addr作为一个参数,不是一个长整型值。
[*]inet_ntoa() 返回一个字符指针,它指向一个定义在函数 inet_ntoa() 中的 static 类型字符串.
[/list]

3.基本套接字调用
Linux 支持伯克利(BSD)风格的套接字编程.它同时支持面向连接和不连接类型的套接字。在面向连接的通讯中服务器和客户机在交换数据之前先要建立一个连接.再不连接通讯中数据被作为信息的一部分被交换.无论那一种方式,服务器总是最先启动,把自己绑定(Banding)在一个套接字上,然后侦听信息.服务器究竟怎样试图去侦听就得依靠你编程所设定的连接的类型了。
3.1、socket() 函数
#include 
#include

int socket(int domain, int type, int protocol);

domain 需要被设置为 “AF_INET” ,就像上面的 struct sockaddr_in。然后,type参数告诉内核这个

socket 是什么类型, “SOCK_STREAM”或是“SOCK_DGRAM” 。最后,只需要把 protocol设置为 0 。
注意:事实上,domain 参数可以取除了“AF_INET”外的很多值,types 参数也可以取除了

“SOCK_STREAM”或“SOCK_DGRAM”的另外类型。具体可以参考socket 的man pages(帮助页)。

3.2bind()函数
#include
#include
int bind (int sockfd , struct sockaddr *my_addr , int addrlen) ;
参数说明:
[list]
[*]sockfd 是由socket()函数返回的套接字描述符。
[*]my_addr 是一个指向 struct sockaddr 的指针,包含有关你的地址的信息:名称端口和 IP地址。
[*]addrlen 可以设置为 sizeof(struct sockaddr)。
[/list]

好,下面我们看一段程序:
#include 
#include
#include
#define MYPORT 4000
main()
{
int sockfd ;
struct sockaddr_in my_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 错误的代码!! */
bind (sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
……
}


最后注意有关 bind()的是:有时候你并不一定要调用 bind()来建立网络连接。比如你只是想连接到一个远程主机上面进行通讯,你并不在乎你究竟是用的自己机器上的哪个端口进行通讯(比如 Telnet) ,那么你可以简单的直接调用 connect()函数,connect()将自动寻找出本地机器上的一个未使用的端口,然后调用 bind()来将其 socket 绑定到那个端口上。

3.3 connect()函数
#include 
#include
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
[list]
[*]sockfd 套接字文件描述符,由socket()函数返回的。
[*]serv_addr 是一个存储远程计算机的IP地址和端口信息的结构。
[*]addrlen 应该是 sizeof(struct sockaddr)。
[/list]

下面让我们来看看下面的程序片段:
#include 
#include
#include

#define DEST_IP "166,111,69.52"
#define DEST_PROT 23

main()
{
int sockfd;
struct sockaddr_in dest_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);
bzreo(&(dest_addr.sin_zero),8);

/* 不要忘记在你的代码中对connect()进行错误检查!! */
connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
……

}
再次强调,一定要检测 connect()的返回值:如果发生了错误(比如无法连接到远程主机,或是远程主机的指定端口无法进行连接等)它将会返回错误值-1。 全局变量 errno将会存储错误代码。另外,注意我们没有调用 bind()函数。基本上,我们并不在乎我们本地用什么端口来通讯,是不是?我们在乎的是我们连到哪台主机上的哪个端口上。Linux 内核自动为我们选择了一个没有被使用的本地端口。

3.4 listen()函数
listen()函数是等待别人连接,进行系统侦听请求的函数。当有人连接你的时候,你两步需要做:通过 listen()函数等待连接请求,然后使用 accept()函数来处理。
listen()函数调用是非常简单的。函数声明如下:
#include 
int listen(int sockfd, int backlog);


listen()函数的参数意义如下:
[list]
[*]sockfd 是一个套接字描述符,由socket()系统调用获得。
[*]backlog 是未经过处理的连接请求队列可以容纳的最大数目。
[/list]

如果你想在一个端口上接受外来的连接请求的话,那么函数的调用顺序为:
socket() ;
bind() ;
listen() ;
/* 在这里调用 accept()函数 */
……
下面将不给出例程,因为 listen()是非常容易理解的。下面的 accept()函数说明中的例程中,有 listen()的使用。

3.5、accept()函数
下面是 accept()函数的声明:
#include 
int accept(int sockfd, void *addr, int *addrlen);


accept()函数的参数意义如下:
[list]
[*]sockfd 是正在 listen() 的一个套接字描述符。
[*]addr 一般是一个指向 struct sockaddr_in 结构的指针;里面存储着远程连接过来的计算机的信息(比如远程计算机的 IP 地址和端口)。
[/list]addrlen是一个本地的整型数值,在它的地址传给 accept()前它的值应该是sizeof(structsockaddr_in);accept()不会在 addr 中存储多余 addrlen bytes 大小的数据。如果accept()函数在addr 中存储的数据量不足 addrlen,则 accept()函数会改变 addrlen 的值来反应这个情况。
PS:如果调用 accept()失败的话,accept()函数会返回–1来表明调用失败,同时全局变量 errno 将会存储错误代码


#include 
#include
#include
/* 用户连接的端口号 */
#define MYPORT 4000
#define BACKLOG 10
main()
{
/* 用来监听网络连接的套接字sock_fd,用户连入的套接字使用new_fd*/
int sockfd , new_fd;
struct sockaddr_in my_addr;
struct sockaddr_in their_addr;

int sin_size;
/*记得在自己的程序中这部分要进行错误检查!*/

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=INADDR_ANY
bzero(&my_addr.sin_zero),8);

bind(sockfd,(struct sockaddr *)&my_addr,sizeof(sockaddr));
listen(sockfd,BACKLOG);
sin_size=sizeof(struct sockaddr_in);
new_fd=accept(sockfd,&their_addr,&sin_size);
……
……

}


面向连接的通信中客户机要做如下一些事:
调用 socket()函数创建一个套接字。
调用 connect()函数试图连接服务。
如果连接成功调用 write()函数请求数据,调用 read()函数接收引入的应答。

3.6、send()、recv()函数
这两个函是最基本的
#include 
#include
int send(int sockfd, const void *msg, int len, int flags);

send 的参数含义如下:
[list]
[*]sockfd 是代表你与远程程序连接的套接字描述符。
[*]msg 是一个指针,指向你想发送的信息的地址。
[*]len 是你想发送信息的长度。
[*]flags 发送标记。一般都设为 0(你可以查看 send 的man pages 来获得其他的参数值并且明白各个参数所代表的含义)。
[/list]

下面看看有关 send()函数的代码片段:
char *msg = “ Hello! World!”;
int len, bytes_sent;
……
……
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
……
……
……
send()函数在调用后会返回它真正发送数据的长度。
注意:send() 所发送的数据可能少于你给它的参数所指定的长度!
因为如果你给 send()的参数中包含的数据的长度远远大于 send()所能一次发送的数据,则 send()函数只发送它所能发送的最大数据长度,然后它相信你会把剩下的数据再次调用它来进行第二次发送。所以,记住如果send()函数的返回值小于 len 的话,则你需要再次发送剩下的数据。幸运的是,如果包足够小(小于1K) ,那么send()一般都会一次发送光的。

下面我们来看看 recv()函数。
函数 recv()调用在许多方面都和send()很相似,下面是recv()函数的声明:
#include 
#include
int recv(int sockfd, void *buf, int len, unsigned int flags);


recv()的参数含义如下:
[list]
[*]sockfd 是你要读取数据的套接字描述符。
[*]buf 是一个指针,指向你能存储数据的内存缓存区域。
[*]len 是缓存区的最大尺寸。
[*]flags 是 recv() 函数的一个标志, 一般都为0 (具体的其他数值和含义请参考 recv()的 man pages)。
[/list]

recv() 返回它所真正收到的数据的长度。(也就是存到 buf 中数据的长度) 。如果返回– 1 则代表发生了错误(比如网络以外中断、对方关闭了套接字连接等) ,全局变量 errno 里面存储了错误代码。


3.7 、sendto() 和recvfrom() 函数
这两个函数是进行无连接的 UDP 通讯时使用的。使用这两个函数,则数据会在没有建立过任何连接的网络上传输。因为数据报套接字无法对远程主机进行连接,想想我们在发送数据前需要知道些什么呢?
PS:取得远程主机的 IP 地址和端口!

下面是 sendto()函数和 recvfrom()函数的声明:
#include 
#include
int sendto (int sockfd, const void *msg, int len, unsigned int flags,const struct sockaddr * to, int tolen);
[list]
[*]sockfd 是代表你与远程程序连接的套接字描述符。
[*]msg 是一个指针,指向你想发送的信息的地址。
[*]len 是你想发送信息的长度。
[*]flags 发送标记。一般都设为 0。 (你可以查看 send 的 man pages 来获得其他的参数值并且明白各个参数所代表的含义)
[*]to 是一个指向struct sockaddr 结构的指针,里面包含了远程主机的IP 地址和端口
[*]数据。
[*]tolen 只是指出了 struct sockaddr 在内存中的大小sizeof(struct sockaddr)。
[/list]
和 send()一样,sendto()返回它所真正发送的字节数(当然也和 send()一样,它所真正发送的字节数可能小于你所给它的数据的字节数)。 当它发生错误的时候,也是返回–1 ,同时全局变量 errno 存储了错误代码。


recvfrom()的声明为:
#include 
#include
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr * from, int

* fronlen)

[list]
[*]sockfd 是你要读取数据的套接字描述符。
[*]buf 是一个指针,指向你能存储数据的内存缓存区域。
[*]len 是缓存区的最大尺寸。
[*]flags 是 recv() 函数的一个标志,一般都为0具体的其他数值和含义请参考 recv()的man pages。
[*]from 是一个本地指针,指向一个struct sockaddr的结构(里面存有源IP 地址和端口数).
[*]fromlen 是一个指向一个 int 型数据的指针,它的大小应该是 sizeof(struct sockaddr) .当函数返回的时候,formlen 指向的数据是 form 指向的 struct
[*]sockaddr 的实际大小.
[/list]

recvfrom() 返回它接收到的字节数,如果发生了错误,它就返回– 1 ,全局变量 errno存储了错误代码.如果一个信息大得缓冲区都放不下,那么附加信息将被砍掉。该调用可以立即返回,也可以永久的等待。这取决于你把 flags 设置成什么类型。你甚至可以设置超时(timeout)值。在说明书(man pages)中可以找到recvfrom的更多信息。


注意:如果你使用 cnnect()连接到了一个数据报套接字的服务器程序上,那么你就可以使用 send() 和 recv() 函数来传输你的数据.不要以为你在使用一个流式的套接字,你所使用的仍然是一个使用者数据报的套接字,只不过套接字界面在 send() 和 recv()的时候自动帮助你加上了目标地址,目标端口的信息.

你可能感兴趣的:(Linux/Unix)