分类: C/C++
2014-06-03 17:14:04
原文地址:Unix环境下的Socket编程--详细解读+源码实现 作者:liubonan
本文先对socket进行简单的介绍,然后详细讲解socket编程的步骤及每一步作用,最后通过一个实例实现客户端与服务器端的通信,代码经测试可用。
1 、什么是 Socket ?
Socket 接口是TCP/IP 网络的 API,Socket 接口定义了许多函数或例程,程序员可以用它们来开发 TCP/IP 网络上的应用程序。要学 Internet 上的 TCP/IP 网络编程,必须理解 Socket 接口。
Socket 接口设计者最先是将接口放在 Unix 操作系统里面的。如果了解 Unix 系统的输入和输出的话,就很容易了解 Socket 了。网络的 Socket 数据传输是一种特殊的 I/O , Socket 也是一种文件描述符 。 Socket 也具有一个类似于打开文件的函数调用 Socket() ,该函数返回一个整型的 Socket 描述符,随后的连接建立、数据传输等操作都是通过该 Socket 实现的。常用的 Socket 类型有两种:
流式 Socket ( SOCK_STREAM )和数据报式 Socket ( SOCK_DGRAM )。
流式是一种面向连接的 Socket ,针对于面向连接的 TCP 服务应用;
数据报式 Socket 是一种无连接的 Socket ,对应于无连接的 UDP 服务应用。
2 、 Socket 建立
为了建立 Socket ,程序可以调用 Socket 函数,该函数返回一个类似于文件描述符的句柄。
socket 函数原型为: int socket(int domain, int type, int protocol);
domain 指明所使用的协议族,通常为 AF_INET ,表示互联网协议族( TCP/IP 协议族); type 参数指定 socket 的类型: SOCK_STREAM 或 SOCK_DGRAM , Socket 接口还定义了原始 Socket ( SOCK_RAW ),允许程序使用低层协议;
protocol 通常赋值 "0" 。
Socket() 调用返回一个整型 socket 描述符,你可以在后面的调用使用它 。
Socket 描述符是一个指向内部数据结构的指针 ,它指向描述符表入口。调用 Socket函数时,socket执行体将建立一个 Socket ,实际上 " 建立一个 Socket" 意味着为一个Socket数据结构分配存储空间。Socket执行体为你管理描述符表。
两个网络程序之间的一个网络连接包括五种信息:通信协议 、本地协议地址 、本地主机端口 、远端主机地址 和远端协议端口 。 Socket 数据结构中包含这五种信息。
3 、 Socket 配置
通过 socket 调用返回一个 socket 描述符后,在使用 socket 进行网络传输以前,必须配置该 socket.
面向连接 的 socket 客户端 通过调用 Connect 函数在 socket 数据结构中保存本地和远端信息。(ps:这时候客户端这边不需要bind,服务器端需要bind)
无连接socket的客户端和服务端以及面向连接 socket 的服务端通过调用 bind 函数来配置本地信息。Bind 函数将 socket 与本机上的一个端口相关联,随后你就可以在该端口监听服务请求。
Bind 函数原型:
int bind(int sockfd,struct sockaddr *my_addr, int addrlen);
Sockfd 是调用 socket 函数返回的 socket 描述符 ,my_addr 是一个指向包含有本机 IP 地址及端口号等信息的 sockaddr 类型的指针; addrlen 常被设置为 sizeof(struct sockaddr) 。
struct sockaddr 结构类型是用来保存 socket 信息的:
struct sockaddr {
unsigned short sa_family; /* 地址族, AF_xxx */
char sa_data[14]; /* 14 字节的协议地址 */
};
sa_family 一般为 AF_INET ,代表 Internet ( TCP/IP )地址族;sa_data 则包含该 socket 的 IP 地址和端口号。
另外还有一种结构类型 :
struct sockaddr_in {
short int sin_family; /* 地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP 地址 */
unsigned char sin_zero[8]; /* 填充 0 以保持与struct sockaddr 同样大小 */
};
这个结构更方便使用。 sin_zero 用来将 sockaddr_in 结构填充到与 struct sockaddr 同样的长度,可以用 bzero() 或 memset() 函数将其置为零。指向 sockaddr_in 的指针和指向 sockaddr 的指针可以相互转换,这意味着如果一个函数所需参数类型是 sockaddr 时,你可以在函数调用的时候将一个指向 sockaddr_in 的指针转换为指向 sockaddr 的指针;或者相反。
使用 bind 函数时,可以用下面的赋值实现自动获得本机 IP 地址和随机获取一个没有被占用的端口号:
my_addr.sin_port = 0; /* 系统随机选择一个未被使用的端口号 */
my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本机 IP 地址 */
通过将 my_addr.sin_port 置为0,函数会自动为你选择一个未占用的端口来使用。
同样,通过将 my_addr.sin_addr.s_addr 置为 INADDR_ANY ,系统会自动填入本机 IP 地址。注意在使用 bind 函数是需要将 sin_port 和 sin_addr 转换成为网络字节优先顺序 ;而 sin_addr 则不需要转换。
计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。 Internet 上数据以高位字节优先顺序在网络上传输 ,所以对于在内部是以低位字节优先方式存储数据的机器,在 Internet 上传输数据时就需要进行转换,否则就会出现数据不一致。
下面是几个字节顺序转换函数:
·htonl() :把 32 位值从主机字节序转换成网络字节序
·htons() :把 16 位值从主机字节序转换成网络字节序
·ntohl() :把 32 位值从网络字节序转换成主机字节序
·ntohs() :把 16 位值从网络字节序转换成主机字节序
Bind() 函数在成功被调用时返回 0 ;出现错误时返回 "-1" 并将 errno 置为相应的错误号。
需要注意的是,在调用 bind 函数时一般不要将端口号置为小于 1024 的值 ,因为 1 到 1024 是保留端口号 ,你可以选择大于 1024 中的任何一个没有被占用的端口号。
4 、连接建立
面向连接的客户程序使用 Connect 函数来配置 socket 并与远端服务器建立一个 TCP 连接,其函数原型为:
int connect(int sockfd, struct sockaddr *serv_addr,int addrlen);
Sockfd 是 socket 函数返回的 socket 描述符;
serv_addr 是包含远端主机 IP 地址和端口号的指针;
addrlen 是远端地址结构的长度。
Connect 函数在出现错误时返回 -1 ,并且设置 errno 为相应的错误码。
进行客户端程序设计无须调用 bind() ,因为这种情况下只需知道目的机器的 IP 地址 ,而客户通过哪个端口与服务器建立连接并不需要关心 , socket 执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候到打断口。
Connect 函数启动和远端主机的直接连接。只有面向连接的客户程序使用 socket 时才需要将此 socket 与远端主机相连 。无连接协议从不建立直接连接。面向连接的服务器也从不启动一个连接,它只是被动的在协议端口监听客户的请求。
Listen 函数使 socket 处于被动的监听模式,并为该 socket 建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们。
int listen(int sockfd , int backlog);
Sockfd 是 Socket 系统调用返回的 socket 描述符;
backlog 指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待 accept() 它们(参考下文)。 Backlog 对队列中等待服务的请求的数目进行了限制,大多数系统缺省值为 20 。如果一个服务请求到来时,输入队列已满,该 socket 将拒绝连接请求,客户将收到一个出错信息。当出现错误时 listen 函数返回 -1 ,并置相应的 errno 错误码。
accept() 函数让服务器接收客户的连接请求 。在建立好输入队列后,服务器就调用 accept 函数,然后睡眠并等待客户的连接请求。
int accept(int sockfd, void *addr, int *addrlen);
sockfd 是被监听的 socket 描述符, addr 通常是一个指向 sockaddr_in 变量的指针,该变量用来存放提出连接请求服务的主机的信息(某台主机从某个端口发出该请求); addrlen 通常为一个指向值为 sizeof(struct sockaddr_in) 的整型指针变量。出现错误时 accept 函数返回 -1 并置相应的 errno 值。
首先,当 accept 函数监视的 socket 收到连接请求时, socket 执行体将建立一个新的 socket ,执行体将这个新 socket 和请求连接进程的地址联系起来,收到服务请求的初始 socket 仍可以继续在以前的 socket 上监听,同时可以在新的 socket 描述符上进行数据传输操作。
5 、数据传输
send() 和 recv() 这两个函数用于面向连接 的 socket 上进行数据传输。
send() 函数原型为:
int send(int sockfd, const void *msg, int len, int flags);
Sockfd 是你想用来传输数据的 socket 描述符;
msg 是一个指向要发送数据的指针;
Len 是以字节为单位的数据的长度;
flags 一般情况下置为 0 (关于该参数的用法可参照 man 手册)。
Send() 函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序中应该将 send() 的返回值与欲发送的字节数进行比较。当 send() 返回值与 len 不匹配时,应该对这种情况进行处理。
char *msg = "Hello!";
int len, bytes_sent;
……
len = strlen(msg);
bytes_sent = send(sockfd, msg,len,0);
……
recv() 函数原型为:
int recv(int sockfd,void *buf,int len,unsigned int flags);
Sockfd 是接受数据的 socket 描述符;
buf 是存放接收数据的缓冲区;
len 是缓冲的长度。
Flags 也被置为 0 。
Recv() 返回实际上接收的字节数,当出现错误时,返回 -1 并置相应的 errno 值。
sendto() 和 recvfrom() 用于在无连接的数据报 socket 方式下进行数据传输。由于本地 socket 并没有与远端机器建立连接,所以在发送数据时应指明目的地址。
sendto() 函数原型为:
int sendto(int sockfd, const void *msg,int len,unsigned int flags,
const struct sockaddr *to, int tolen);
该函数比 send() 函数多了两个参数, to 表示目地机的 IP 地址和端口号信息,而 tolen 常常被赋值为 sizeof (struct sockaddr) 。 sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回 -1 。
recvfrom() 函数原型为:
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
from 是一个 struct sockaddr 类型的变量,该变量保存源机的 IP 地址及端口号。
fromlen 常置 sizeof (struct sockaddr) 。当 recvfrom() 返回时, fromlen 包含实际存入 from 中的数据字节数。Recvfrom() 函数返回接收到的字节数或当出现错误时返回-1,并置相应的 errno 。
如果你对数据报 socket 调用了 connect() 函数时,你也可以利用 send() 和 recv() 进行数据传输 ,但该 socket 仍然是数据报 socket ,并且 利用传输层的 UDP 服务 。但在发送或接收数据报时, 内核会自动为之加上目地和源地址信息 。
6 、结束传输
当所有的数据操作结束以后,你可以调用 close() 函数来释放该 socket ,从而停止在该 socket 上的任何数据操作: close(sockfd);
你也可以调用 shutdown() 函数来关闭该 socket 。该函数允许你只停止 在某个方向上的数据传输,而一个方向上的数据传输继续进行。如你可以关闭某 socket 的写操作而允许继续在该 socket 上接受数据,直至读入所有数据。
int shutdown(int sockfd,int how);
Sockfd 是需要关闭的 socket 的描述符。参数 how 允许为 shutdown 操作选择以下几种方式:
·0------- 不允许继续接收数据
·1------- 不允许继续发送数据
·2------- 不允许继续发送和接收数据,
· 均为允许则调用 close ()
shutdown 在操作成功时返回 0 ,在出现错误时返回 -1 并置相应 errno 。
7 、面向连接的 Socket 实例
代码实例中的服务器通过 socket 连接客户端。
该服务器软件代码如下:
#include
#include
#include
#include
#include
#include
int net_server_init ( const struct sockaddr_in *addr, int addr_len);
int main ()
{
int sockfd;
int addr_len;
struct sockaddr_in server_addr;
int port;
port = 5003;
addr_len = sizeof ( struct sockaddr_in);
server_addr. sin_family = AF_INET;
server_addr. sin_addr . s_addr = INADDR_ANY; //本机地址
server_addr. sin_port = htons (port);
//connect to client
sockfd = net_server_init(&server_addr, addr_len);
if (!(sockfd<0)){
printf "connect OK!/n" );
}
else {
printf( "connect error!/n" );
}
close (sockfd);
return 0;
}
int net_server_init ( const struct sockaddr_in *addr, int addr_len)
{
int socket_fd; //socket descriptor
int optval;
socket_fd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (socket_fd == -1) {
printf( "Failed to init socket/n" );
return -1;
}
/*--Reuse ip address and port.*/
optval = 1;
if ( setsockopt (socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval,
sizeof (optval)) < 0) {
printf( "Failed to set address reuse./n" );
}
if ( bind (socket_fd, ( struct sockaddr *)addr, addr_len) < 0) {
printf( "Failed to bind socket: %s/n" );
return -1;
}
if ( listen (socket_fd, BACKLOG) < 0) {
printf( "Failed to listen socket: %s/n" );
return -1;
}
return socket_fd;
}
服务器的工作流程是这样的:首先调用 socket 函数创建一个 Socket ,然后调用 bind 函数将其与本机地址以及一个本地端口号绑定,然后调用 listen 在相应的 socket 上监听,当 accpet 接收到一个连接服务请求时,将生成一个新的 socket 。
客户端程序代码如下:
#include
#include
#include
#include
#include
#include
int net_client_init ( const char *ip, int port);
int main ()
{
int sockfd;
//connect to server
sockfd = net_client_init( "192.168.1.96" , 5003);
if (!(sockfd<0)){
printf( "connect OK!/n" );
}
else {
printf( "connect error!/n" );
}
close(sockfd);
return 0;
}
int net_ client_ init ( const char *ip, int port)
{
int socket_fd;
int addr_len;
struct sockaddr_in *client_addr;
client_addr=(struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));
memset (client_addr, 0, sizeof ( struct sockaddr_in));
addr_len = sizeof ( struct sockaddr_in);
client_addr-> sin_family =AF_INET;
//convert string ip to 32 bit net serial ip
if ( inet_aton (ip, &client_addr-> sin_addr ) == 0){
printf("failed to convert ip address form./n" );
return -1;
}
client_addr-> sin_port = htons (port); //2 byte host to net
socket_fd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (socket_fd == -1) {
printf("Failed to init socket/n" );
return -1;
}
if ( connect (socket_fd, ( const struct sockaddr *)client_addr, addr_len) < 0){
printf("Failed to connect/n" );
return -1;
}
free (client_addr);
client_addr = NULL;
return socket_fd;
}
在客户端和服务器端分别用gcc编译,然后先运行服务器端可执行程序,这时候服务器端处于监听状态,然后在运行客户端可执行程序,连接服务器。连接成功则分别输出“connect ok!”。