先从底层说起,Socket是如何实现的?
图1,Linux下收包。可见Socket是由操作系统内核在MAC层——IP层——传输层上实现的。
真正从网卡(NIC)进来的数据是完整的MAC帧,底层用数据结构sk_buff 描述,最终进入接收缓冲区recv buffer,而我们应用层调用read / recv /recvfrom 从接收缓冲区拷贝数据到应用层提供的buffer,对一般的套接字,如SOCK_STREAM(TCP), SOCK_DGRAM(UDP) 来说,此时缓冲区只有user data,其他各层的头部已经被去除,而对于SOCK_RAW 来说是IP head + IP payload,当然也可以是arp/rarp 包,甚至是完整的帧(加上MAC头)。
下面介绍Socket编程的基本步骤:
涉及到的数据结构:
Sockaddr地址的统一表示:
- 地址家族号 short
- 地址具体值 14字节
struct sockaddr_in {//用于TCP/IP家族的地址表示
short sin_family; //地址家族号,为扩展提供可能,一般取0,表示TCP/IP家族
u_short sin_port; //端口号
struct in_addr sin_addr; //存ip地址,具体见下图
char sin_zero[8]; //填充位,取8个0
};
Host Entry:可通过gethostbyname(char *)获取hostent*;
struct
hostent{
char
* h_name; //地址正式名称
char
** h_aliases; //别名数组
short
h_addrtype; //地址类型,一般取AF_INET
short
h_length; //地址长度(bit)
char
** h_addr_list; //IP列表,网络字节序
#define h_addr h_addr_list[0]; //第一个地址
};
点分十进制和无符号长整型互相转换
- long inet_addr(char* ip)点分十进制转网络字节序
- char* inet_ntoa(in_addr)网络字节序转点分十进制
方法介绍:
服务器端的sockaddr_in.sin_addr可以用IN_ADDRANY
创建socket的参数:
- 协议簇的类型,即指定通信域现在为TCP/IP PF-INET。
- 传输层协议:SOCK_STREAM字节流(TCP)||SOCK_DGRAMA数据报文(UDP)
- 网络层协议:一般为0指ip
- 创建后会自动绑定ip和随机端口号
Bind:
- Socket描述符
- 网络进程的地址信息,包括ip,port等
- 地址信息这个结构体的长度
- 一定要注意,一般只在listen的服务器端口进行绑定。客户端和服务器并发新开的线程没有该过程,因为他们的交互是短暂的,用完就将资源交还给OS。服务器绑定是因为提供的服务是约定好的。
- 另外 绑定是包括ip的,这对多网卡服务器是有意义的。
Listen:成功返回1,失败小于0
- Socket描述符
- 等待队列的最大长度,最大20一般5-10
- 注意UDP方式则不需要提供该操作,因为是无连接的,接收方不需要等待。
- 服务器收到请求,队列不满,则放到缓冲队列,用accept调用。
Accept(默认阻塞):
- 服务器端用于listen的Socket描述符,以提供服务器的地址信息
- Sockaddr指针,所接收客户端的网络地址信息。是出参,只给定义即可
- 上述地址的长度信息
- 如果连接成功,系统将返回一个新的套接字描述符int,该套接字此后专用于与该客户机的数据交换。但需要注意,这返回的仅仅是新建好的一个套接字描述符,应该建立线程处理用户的请求。
- Accept会有阻塞,缓冲队列取不到东西,则一直等待,使得主进程无法继续进行。而listen只是表示开始监听,指定哪个地方是接收请求的,缓冲队列长度,指定完就返回,不存在阻塞。
Connect(默认阻塞):
- 客户机的socket标识符
- 服务器的网络地址信息
- 地址长度
Read/write/send/recv(均为默认阻塞):
- 自己的套接字标识符
- 读写缓冲区指针
- 缓冲区长度
Close:
- 关闭连接前应先终止连接
TCP通信,C++Code:
/*------------------------------------------
* 程序:server.c
* 目的: 分配一个套接字,然后反复执行如下几步:
* (1) 等待客户的下一个连接
* (2) 发送一个短消息给客户
* (3) 关闭与客户的连接
* (4) 转向(1)步
* 命令行语法: server [ port ]
* port – 服务器端监听套接字使用的协议端口号
* 注意: 端口号可选。如果未指定端口号,服务器使用PROTOPORT中指定的缺省* 端口号
*-----------------------------------------------*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#define PROTOPORT 5188 /* 监听套接字的缺省协议端口号 */
#define QLEN 6 /* 监听套接字的请求队列大小 */
int visits = 0; /* 对于客户连接的计数*/
main(argc,argc)
int argc;
char* argv[];
{
struct hostent *ptrh; /* 指向主机列表中一个条目的指针 */
struct sockaddr_in servaddr; /* 存放服务器网络地址的结构 */
struct sockaddr_in clientaddr; /* 存放客户网络地址的结构 */
int listenfd; /* 监听套接字描述符 */
int clientfd; /* 响应套接字描述符 */
int port; /* 协议端口号 */
int alen; /* 地址长度 */
char buf[1000]; /* 供服务器发送字符串所用的缓冲区 */
/* 清空sockaddr结构 */
memset( (char*)& servaddr, 0, sizeof(servaddr) );
servaddr.sin_family = AF_INET; /* 设置为因特网协议族 */
servaddr.sin_addr.s_addr = INADDR_ANY; /* 设置本地IP地址 */
/* 检查命令行参数,若已指定,则使用该端口号,否则使用缺省端口号 */
if (argc > 1){
port = atoi(argv[1]); /* 如果指定了端口号,就将它转换成整数 */
}
else {
port = PROTOPORT; /* 否则,使用缺省端口号 */
}
/* 将本地地址绑定到监听套接字*/
if ( bind( listenfd, (struct sockaddr *)& servaddr, sizeof(servaddr)) < 0) {
fprintf(stderr, ”bind failed\n” );
exit(1);
}
/* 开始监听,并指定监听套接字请求队列的长度 */
if (listen(listenfd, QLEN) < 0) {
fprintf(stderr, ”listen filed\n” );
exit(1);
}
/* 服务器主循环—接受和处理来自客户端的连接请求 */
while(1) {
alen = sizeof(clientaddr);
/* 接受客户端连接请求,并生成响应套接字 */
if((clientfd = accept( listenfd, (struct sockaddr *)& clientaddr, &alen)) < 0 ) {
fprintf( stderr, “accept failed\n”);
exit(1);
}
visits++; /* 累加访问的客户数 */
sprintf( buf, “this server has been contacted %d time \n”, visits );
send(clientfd, buf, strlen(buf), 0 ); /* 向客户端发送信息 */
closesocket( clientfd ); /* 关闭响应套接字 */
}
}
/*---------------------------------------------------- * 程序: client.c * 目的: 创建一个套接字,通过网络连接一个服务器,并打印来自服务器的信息 * 语法: client [ host [ port ] ] * host - 运行服务器的计算机的名字 * port - 服务器监听套接字所用协议端口号 * 注意:两个参数都是可选的。如果未指定主机名,客户使用localhost;如果未指定端口号, * 客户将使用PROTOPORT中给定的缺省协议端口号 *----------------------------------------------------*/ #include <sys/types.h> /* 基本系统数据类型*/ #include <sys/socket.h> /* UNIX下,套接字接口包含文件*/ #include <netinet/in.h> /* Internet地址簇*/ #include <arpa/inet.h> /* Internet定义*/ #include <netdb.h> /* 网络数据库操作*/ #include <stdio.h> /* 标准输入输出*/ #include <string.h> /* 字符串处理*/ #define PROTOPORT 5188 /*缺省协议端口号*/ extern int errno; char localhost = “localhost”; /*缺省主机名*/ main(argc,argv) int argc; char *argv[]; { struct hostent *ptrh; /* 指向主机列表中一个条目的指针 */ struct sockaddr_in servaddr; /* 存放服务器端网络地址的结构 */ int sockfd; /* 客户端的套接字描述符 */ int port; /* 服务器端套接字协议端口号*/ char* host; /* 服务器主机名指针 */ int n; /* 读取的字符数 */ char buf[1000] ; /* 缓冲区,接收服务器发来的数据 */ memset((char*)& servaddr,0,sizeof(servaddr)); /* 清空sockaddr结构 */ servaddr.sin_family = AF_INET; /* 设置为因特网协议族 */ /* 检查命令行参数,如果有,就抽取端口号。否则使用内定的缺省值*/ if (argc>2){ port = atoi(argv[2]); /* 如果指定了协议端口,就转换成整数 */ } else { port = PROTOPORT; /* 否则,使用缺省端口号 */ } if (port>0) /* 如果端口号是合法的数值,就将它装入网络地址结构 */ servaddr.sin_port = htons((u_short)port); else{ /* 否则,打印错误信息并退出*/ fprintf(stderr,”bad port number %s\n”,argv[2]); exit(1); } /* 检查主机参数并指定主机名 */ if(argc>1){ host = argv[1]; /* 如果指定了主机名参数,就使用它 */ }else{ host = localhost; /* 否则,使用缺省值 */ } /* 将主机名转换成相应的IP地址并复制到servaddr 结构中 */ /* 从服务器主机名得到相应的IP地址 */ ptrh = gethostbyname( host ); if ( (char *)ptrh == null ) { /* 检查主机名的有效性,无效则退出 */ fprintf( stderr, ”invalid host: %s\n”, host ); exit(1); } memcpy(&servaddr.sin_addr, ptrh->h_addr, ptrh->h_length );
/* 创建一个套接字*/ sockfd = SOCKET(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { fprintf(stderr, ”socket creation failed\n” ); exit(1); } /* 请求连接到服务器 */ if (connect( sockfd, (struct sockaddr *)& servaddr, sizeof(servaddr)) < 0) { fprintf( stderr,”connect failed\n” ); /*拒绝连接,报错退出 */ exit(1); }
/* 从套接字反复读数据,并输出到用户屏幕上 */ n = recv(sockfd , buf, sizeof( buf ), 0 ); while ( n > 0) { write(1,buf, n); n = recv( sockfd , buf, sizeof( buf ), 0 ); } /* 关闭套接字*/ closesocket( sockfd ); /* 终止客户程序*/ exit(0); }参考:http://blog.csdn.net/jnu_simba/article/details/12371127 《浅谈原始套接字》