概述
存在三种套接字:流式套接字(SOCK_STREAM)、数据报套接字(SOCK_DGRAM)和原始套接字(SOCK_RAW)。
TCP套接字工作流程:
首先,服务器端启动进程,调用Socket创建一个基于TCP协议的流套接字描述符。
其次,服务进程调用bind命名套接字,将套接字描述符绑定到本地地址和本地端口上。
再次,服务器端调用listen,开始侦听客户端的Socket连接请求。
接下来,客户端创建套接字描述符,并且调用connect向服务器端提交连接请求。服务器端接收到客户端连接请求后,调用accept,接受并创建一个新的套接字描述符与客户端建立连接,然后原套接字描述符继续侦听客户端的连接请求。
客户端与服务器端新套接字进行数据传输,调用write或send向对方发送数据,调用read或recv接收数据。
在数据交流完毕后,双方调用close或者shutdown关闭套字。
socket的创建
函数原型:
intsocket(int domain, int type, int protocol);
PS:在实际编程中,我们只使用AF_INET协议,如果需要与本地主机进程建立连接,只需把远程地址设定为“127.0.0.1”即可。
创建套接实例:
创建AF_INET协议族上的流套接字描述符。
socket(AF_INET,SOCK_STREAM, 0)
或
socket(AF_INET,SOCK_STREAM, TCP)
创建AF_INET协议族上的数据报套接字描述符。
socket(AF_INET,SOCK_DGRAM, 0)
或
socket(AF_INET,SOCK_DGRAM, UDP)
socket的命名
函数bind命名一个套接字,它为该套接字描述符分配一个半相关属性,其原型如下:
intbind(int s, const struct sockaddr* name, int namelen);
套接字地址属性结构:
structsockaddr
{
u_shortsa_family; /*协议族*/
charsa_data[14] /*最多14字节的协议地址*/
}
每个协议族都定义了自己套接字属性结构,协议族AF_INET使用结构sockaddr_in描述套接字地址信息。
structsockaddr_in
{
shortsin_family; /*16位的地址协议族(AF_INET)*/
u_shortsin_port; /*16位的端口地址*/
structin_addr sin_addr; /*32位的IP地址*/
charsin_zero[8]; /*预留,保持sockaddr_in与结构sockaddr的长度相同*/
};
structin_addr
{
u_longs_addr;
}
IP地址转换:
unsignedlong inet_addr(char *ptr);
intinet_aton(char *ptr, struct in_addr *addrptr);
char*inet_ntoa(struct in_addr inaddr);//将IP地址的整数形式转换为字符串格式输出
字节顺序转换:
u_longhtonl(u_long hostlong);
u_shorthtons(u_short hostshort);
u_longntohl(u_long netlong);
u_shortntohs(u_short netshort);
“h”代表“host”,代表主机字节序
“n”代表“network”,代表网络字节序。
“l”代表“long”,代表32位整数
“s”代表“short”,代表16位整数
例子:命名套接字描述符s的协议为AF_INET,地址由系统自动指定,端口号为1000.
structsockaddr_in sockaddr1; /*申请地址结构空间*/
memset(&sockaddr,0, sizeof(sockaddr)); /*清空空间*/
sockaddr1.sin_family= AF_INET; /*指定为AF_INET协议族*/
sockaddr1.sin_addr.s_addr= htonl(INADDR_ANY); /*任何IP地址*/
sockaddr1.sin_port= htons(10000); /*端口号*/
bind(s,(struct sockaddr *)&sockaddr1, sizeof(sockaddr1));
socket的侦听
函数原型:
intlisten(int s, int backlog);
PS:listen仅在流套接字中使用。上述参数分别表示套接字描述符和套接字接受连接的最大数目。
例:服务器端套接字s进入侦听,最多只能接收客户端的10条申请。
listen(s,10);
/*TCP服务器端套接字准备函数*/
intCreateSock(int *pSock, int nPort, int nMax)
{
structsockaddr_in addrin;
structsockaddr *paddr = (struct sockaddr*)&addrin;
ASSERT(pSock!= NULL && nPort > 0 && nMax > 0);
memset(&addrin,0, sizeof(addrin));
addrin.sin_family= AF_INET;
addrin.sin_addr.s_addr= htonl(INADDR_ANY);
addrin.sin_port= htons(nPort);
ASSERT((*pSock=socket(AF_INET,SOCK_STREAM,0)) > 0);
if(VERIFY(bind(*pSock,paddr,sizeof(addrin))>= 0)&& VERIFY(listen(*pSock,nMax) >=0))
return0;
VERIFY(close(*pSock)== 0);
return1;
}
Socket的连接处理
函数原型:
intaccept(int s, struct sockaddr *addr, int *addrlen);
/*TCP服务器端接收连接函数*/
intAcceptSock(int *pSock, int nSock)
{
structsockaddr_in addrin;
intlSize;
ASSERT(pSock!= NULL && nSock > 0);
while(1)
{
lSize= sizeof(addrin);
memset(&addrin,0, sizeof(addrin));
if((*pSock= accept(nSock,(structsockaddr*)&addrin,&lSize))> 0)
return0;
elseif(errno == EINTR)
continue;
else
ASSERT(0);
}
}
socket的关闭
函数原型:
intshutdown(int s, int how);
函数shutdown可以全部关闭或者部分关闭套接字描述符s的连接,参数how指定了套接字关闭的方式,取值含义如下:
How取值 |
描述 |
0 |
套接字不可读,系统将自动丢弃接收到的数据和留在缓冲区中的数据,进程不能再从套接字中接收通信数据 |
1 |
套接字不可写,系统将写缓冲区的数据发送完毕后关闭套接字写操作,进程不能再从套接字中发送通信数据 |
2 |
彻底关闭套接字的连接 |
shutdown是强制性关闭全部套接字连接,而函数close只将套接字访问计数器减1,当且仅当计数器值为0时,系统才真正地关闭套接字通信。
PS:利用close的这个特性,可以建立Socket通信的服务器端的并发管理:父进程首先创建侦听套接字,一旦接收到连接请求则创建新的套接字与客户连接,再fork子进程,随后父进程调用close关闭新创建的套接字后继续侦听,子进程则调用close关闭侦听套接字,全权负责与客户端的通信。
Socket的连接申请
函数原型:
intconnect(int s, const struct sockaddr *name, int namelen);
/*********TCP客户端函数**********/
intConnectSock(int *pSock, int nPort, char *pAddr)
{
structsockaddr_in addrin;
longlAddr;
intnSock;
ASSERT(pSock!= NULL && nPort > 0 && pAddr != NULL);
/***创建TCP套接字描述符***/
ASSERT((nSock= socket(AF_INET, SOCK_STREAM, p)) > 0);
/***协议地址组包*****/
memset(&addrin,0, sizeof(addrin));
addrin.sin_family= AF_INET;
addrin.sin_addr.s_addr= inet_addr(pAddr);
addrin.sin_port= htons(nPort);
if(VERIFY(connect(nSock,(struct sockaddr *)&addrin, sizeof(addrin)) > = 0))
{
/***连接成功,返回套接字描述符***/
*pSock= nSock;
return0;
}
close(nSock);
return1;
}
TCP数据的发送
套接字一旦连接上,就可以发送数据。
函数原型:
intsend(int s, const void *msg, int len, int flags);
flags标志:
MSG_OOB:发送外带数据
MSG_DONTROUTE:通知远程IP就在本地局域网内,消息中不加入跌幅消息
TCP数据的接收
函数原型:
intrecv(int s, void *buf, int len, int flags);
flags标志:
MSG_OOB:接收外带数据
MSG_PEEK:以窥视方式接收数据,即只接收而不从缓冲区中删除数据,下一次调用recv或read仍然可以接收这些数据
MSG_WAITALL:函数阻塞直到读取len字节数为止。不过本地标志并并非完全阻塞,当进程接收到信号,套接字出错、连接中断或指定了MSG_PEEK等情况出现时函数仍然会提前返回
其他一些相关函数
structhostent *gethostbyname(const char *name);
structhostent *gethostbyaddr(const char *addr, int len, int type);
voidherror(const char *string);//不能使用perror
structservent *getservbyname(const char *name, const char *proto);
structservent *getservbyport(int port, const char *proto);
intgetsockname(int s, struct sockaddr *name, int *namelen);
intgetpeername(int s, struct sockaddr *name, int *namelen);
套接字选项
函数原型:
intgetsockopt(int s, int level, int optname, void *optval, int *optlen);
intsetsockopt(int s, int level, int optname, const void *optval, intoptlen);
相关选项说明:
SO_DEBUG
本选项只支持TCP协议,当选项打开时,系统内核跟踪套接字发送和接收的全部TCP数据记录。
SO_REUSEADDR和SO_REUSEPORT
当尝试将一个套接绑定到某个端口时,如果该端口已经被占用了,一般情况下,绑定会失败,如果设置了此选项,系统能够让套接字bind到正在使用的地址或端口上。
SO_KEEPALIVE
本选项是周期性地测试套接字连接是否依然存在。
SO_DONTROUT
本选项控制发送消息是否越过协议的路由机制,打开时套接字将绕过协议的路由机制发送的数据。
SO_LINGER
当缓冲区中还有数据尚未发送时,套接字的默认关闭机制是系统将这些数据立即发送对方,但不等待对方确认接收马上关闭套接字。SO_LINGER可以改变这个设置,它的数据传递通过结构linger完成。linger的结构定义如下:
structlinger
{
intl_onoff;//选项开关
intl_linger;//延时时间
}
结构linger成员取值含义如下:
L_onoff |
L_linger |
关闭机制 |
0 |
忽略 |
默认关闭机制 |
非0 |
0 |
直接丢弃缓冲区,立即关关套接字 |
非0 |
非0 |
等待缓冲区数据发送完毕或者延时l_linger秒后关闭套接字。如果套接字非阻塞选项打开,则立即关闭 |
SO_BROADCAST
控制能否发送套接字数据广播,只用于数据报套接字和支持广播信息的网络上。
SO_OOBINLINE
此选项打开时允许外带数据留在输入队列中,此时函数read和recv可以在不指明MSG_OOB标志的情况下读取外带数据。
SO_SNDBUF和SO_RCVBUF
每个套接都有一个发送缓冲区和接收缓冲区,这两个缓冲区由底层的协议使用,选项SO_SNDBUF可以改变发送缓冲区的大小,选项SO_RCVBUF可以改变接收缓冲区的大小。客户端必须在connect前设置SO_RCVBUF选项,服务器端必须在listen前设置SO_RCVBUF选项,才可以有效地更改缓冲区的容量大小。
SO_SNDLOWAT和SO_RCVLOWAT
这两个选项分别可以改变最小发送数据量和最小接收数据量。
SO_SNDTIMEO和SO_RCVTIMEO
这两个选项用于设置发送数据和接收数据设置一个超时时间。超时时间采用结构timeval描述。
SO_TYPE
调用本选项可以获取套接字的类型,返回值为SOCK_STREAM、SOCK_DGRAM或SOCK_RAW等,此选项不能用于setsockopt中。
SO_ERROR
调用本选项可以获取并且清除套接字错误,不能应用于函数setsockopt中。