最近一直太忙了,周末也忙着和我的徒弟MM聚会,玩得太high了,好久没有更新我的blog了。早就说好要写关于socket的东西的。socket的东西有点,这次写点主要写常用的函数吧,在SOCKET常用的函数和开发模式(2)中会写下socket的应用开发模式。
以windows下的socket编程举例吧。socket用到的类,winsock2.h(winsock.h是老版本),依赖的lib有ws2_32.lib。
声明一个socket,SOCKET Sock.对于SOCKET的定义在winsock2.h中是UINT_PTR,可以看成一个指针类型(其实就是unsigned int类型的,所以可以看成一个端口的编号,或者sock类的指针)。
在使用socket之前,必须要做的一件事情就是初始化socket: InitWinsock();//这个只需要执行一次就可以了。需要在使用之间最先被调用。
然后定义一个socket,定义的方法为SOCKET sock = socket(AF_INET,SOCK_DGRAM,0); //三个参数各有自己的含义,这是UDP的初始化方式,TCP的初始化方式为socket(AF_INET,SOCK_STREAM,0);
然后要将你申请的socket绑定到一个本地的地址和端口上,对于一个端口和地址的描述信息的表示就是用结构体sockaddr_in或者sockaddr,你可以当作两个是一样的.
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_addr.s_addr=INADDR_ANY; //本机所有的IP地址(多IP的时候有区分)
local.sin_port=htons(port); //port为你需要绑定的端口
if(bind(sock,(struct sockaddr*)&local,sizeof local)==SOCKET_ERROR) //将sock绑定到本地的端口上
{
int err = WSAGetLastError(); //出现了错误可以用这个函数捕作错误的返回值,这个值得具体含义,可以在msdn或者Error Lookup中找到对应的ID和意义.
closesocket(sock); //出错了要主动关闭该socket
}
一个socket的初始化就已经完成了.当然可以在bind之前用setsockopt()来设置端口是否可以重用,用ioctlsocket()来设置socket是否阻塞等等操作.这两个函数可以做的事情很多,但是我要说这两个功能的原因是1.在端口不可重用的模式下(默认是该模式),一个端口被bind之后,然后closesocket,这个端口一般不能立即被再次bind,因为系统需要30秒之内来进行释放该端口.2.在阻塞模式下接收数据包的时候(默认是阻塞的,但是调用WSAAsyncSelect和 WSAEventSelect这两个函数的时候,默认会将socket设置为非阻塞的),耗时以及数据传输的稳定性会受到一定的影响(在pc上的影响非常的小,但是对于手机等小型操作系统来说就很明显了),这两个特点在开发的过程中可能要频繁的使用到.
初始化完成之后,然后是socket之间的通信工作了.不一定需要在两台机器上才可以使用socket通信,对于进程之间的数据共享时,socket也是重要的共享手段之一.首先理解一下TCP和UDP的概念,一个是面向连接的,一个是面向无连接的.TCP的通信中需要有连接的概念,那么两者之间的断开和连接,都会让对方知道,包括数据发送时的大小等传输时的参数也需要两边进行通知.UDP是无连接的,只要知道对方的IP和端口,然后按UDP的方式发一个包出去,至于对方是什么状态,是否已经离线这些都是无从关心的,那么允许包的丢失,而且数据的报送和接收是一个UDP包为单位的,不像TCP以数据长度为单位的.那么基于上述的概念,自然它们的建立连接的过程也是有区别的.
TCP的连接需要先做connect操作
if(connect(sock,(sockaddr *)&dest,sizeof(sockaddr))==SOCKET_ERROR) //dest 也是一个sockaddr_in结构的远端的IP地址和端口的描述.
{
int err = WSAGetLastError();
closesocket(sock);
}
而UDP是"没有必要"connect()的,既然是"没有必要",自然UDP也可以connect,只是无法侦测到远端连接的状态,数据发送的情况等等.
连接上了,那么就是数据的接收和发送了.发送和接收的函数有成对的两组,sendto和recvfrom,send和recv,看函数的名字都可以知道,前面两个会带上需要发送和接收的目标地址,后面两个函数只需要发送和接收就可以了.也就是说后者只能在connect之后才使用,因为connect之后,已经知道了对端的IP地址和端口了,只需要发送和接收就好了(这种模式可能会接收到非法的数据)。这两组函数返回的都是数据接收和发送的长度。以send和sendto举例如下,recv和这两个函数类型
sendto(sock,data,size,0,(sockaddr *)&(m_dest),sizeof(sockaddr)); //data为数据的头指针,size为大小,m_dest为目标的地址和端口的sockaddr_in结构。
send(sock,data,size,0);
当系统中需要对多个端口进行侦测时,就要用到select函数了。它的用法是
fd_set read_fds;
int maxfd = 0; //用于记录最大的sock的值
for(int i= 0;i<count;i++)
{
FD_SET(sock_list[i],&read_fds); //循环调用该语句,将对应的所有的SOCKET都生成到read_fds中,sock_list中存放所有的socket
if(sock_list[i]>maxfd)
maxfd = sock_list[i];
}
struct timeval ptv //时间间隔,用于描述select的阻塞时间
ptv.tv_sec = 0;
ptv.tv_usec = 100 *1000; //100ms
int ret = select(maxfd+1,readfds,0,0,ptv); //其中参数2为需要读取SOCKET列表的fd_set,参数3为需要写入的SOCKET列表的fd_set,因为参数3 我写入的是0,也就说目前的含义就是查看是否有需要读的数据。ret>0即为有需要读的数据.
然后循环查询自己的socket列表,如果FD_ISSET(sock,&read_fds)为true,说明该SOCKET有需要读取的数据
PS:当TCP下线的时候,select会返回一个需要读取的事件,但是读取对应的端口时,会返回<=0的值,这时候就是TCP下线了。用到windows的socket的相应的函数时,可能用法上会有些变化,例如WSAEventSelect 等,其实这些函数只是在上述函数上二次的封装而已,并与windows的Message map结合起来。
给出一个sockaddr_in使用的例子。
struct hostent* host = NULL;
sockaddr_in tmpSockAddr;
const char * ip ="192.168.110.22";//或者const char * ip ="Gabe_host"是一个主机名
unsigned long ipAddress = inet_addr(ip);
if(ipAddress ==INADDR_NONE)
{
host = gethostbyname(ip);
if(!host)
{
return -1;
}
else
{
memcpy((char *)&tmpSockAddr.sin_addr,host->h_addr,host->h_length);
}
}
else
{
tmpSockAddr.sin_addr.s_addr = ipAddress;
}
tmpSockAddr.sin_port = htons(80);