又回头看来一次socket的知识点,简单做个总结。有什么问题欢迎指出。顺便打个广告--游戏开发讨论群:462686014
在UNIX中,创建套接字时和文件打开一样,在描述符表中取回一个int类型的索引号。套接字和文件是共享描述符表,因此他们的索引号不能重复,一个进程能同时创建最大的套接字数和文件数是相同的。
下面介绍开发中使用的函数。
int socket(int af, int type, int protocol);socket函数的返回值是新创建的套接字的int型索引。如果套接字创建失败,则socket函数的返回值为 -1,具体错误原因可以通过errno函数查找。 下面是socket() 函数的示例。
/* 套接字的创建函数,创建成功返回套接字索引,创建失败时返回-1 */ int CreateSocket() { int newSocket; newSocket = socket(AF_INET, SOCK_STREAM, 0); if (newSocket < 0) return -1; return newSocket; }当想终止套接字的连接或者套接字的使用结束时,我们需要返还套接字的资源。与文件一样,使用结束的套接字返回是用函数close() 完成。close() 的原型如下:
int close(int d);close的唯一参数是要结束的套接字的索引。这里有一个问题需要考虑的是,如果还存在要传送给对方主机的数据或者还没有处理对方送来的数据,则默认处理方式是close函数等这些未处理的数据梳理完后,返还套接字的资源。 除close函数外,shutdown也能关闭套接字。一般情况下使用close函数居多,但shutdown函数提供更多的选项。shutdown函数的原型如下:
int shutdown(int socket, int direction);
/* 设置套接字为Nonblocking模式 */ void NonBlock(int sock) { int flags = fcntl(sock, F_GETFL, 0); flags |= O_NONBLOCK; if ( fcntl(sock, F_SETFL, flags) < 0 ) exit(1); }fcntl函数是提供获取或修改套接字等文件描述符的属性功能的UNIX函数。
int connect(int s, const struct sockaddr *name, int namelen);当connect函数的调用成功时,即与对方主机连接成功时,函数返回值为0,其他情况返回-1,若需要查看详细错误原因,可以利用errno函数检查。
/* 利用指定的参数int sock,与localhost(127.0.0.1)的8081端口建立连接 成功返回1,失败返回0 */ int ConnectToServer(int sock) { struct sockaddr_in addr_in; addr_in.sin_family = AF_INET; addr_in.sin_addr.s_addr = inet_addr("127.0.0.1"); addr_in.sin_port = htons(8081); if (connect(sock, (struct sockaddr*)&addr_in, sizeof(addr_in) < 0) return 0; return 1; }除了使用ip与主机建立连接外,还可以使用DNS进行连接。用域名时,不能用把ip地址转换成二进制形式的inet_addr函数来获取对方主机目的地址。为了把域名转换成二进制形式的地址,需要使用如下所示的gethostbyname函数。
/* 传送到char* adrr参数的Internet域名地址,转换成long类型地址形式 */ unsigned long GetAddrBydomian(char* addr) { struct hostent *ph; struct in_addr in; ph = gethostname(addr); if (ph == NULL) return NULL; memcpy((char**)&(in), ph->h_addr, ph->h_length); return in.s_addr; }上述函数中,使用gethostname函数把域名转换成hostent结构体返回,在hostent结构体中获取二进制形式的地址。一般引用hostent结构体定义的变量,使用成员h_addr,该成员指向h_addr_list第一个指针变量的地址。下面是利用GetAddrBydomian函数的ConnectToServer的应用示例。
/* 既可以使用ip地址,也可以使用域名的conncet函数 成功返回1,失败返回0 */ int ConnectToServer(int sock,char* address,int port,int isDomain) { struct sockaddr_in addr_in; addr_in.sin_family = AF_INET; addr_in.sin_port = htons(port); if (isDomain) addr_in.sin_addr.s_addr = GetAddrBydomian(address); else addr_in.sin_addr.s_addr = inet_addr(address); if (connect(sock, (struct sockaddr*)&addr_in, sizeof(addr_in) < 0) return 0; return 1; }
int bind(int sock, const struct sockaddr* addr, socklen_t addrlen);如果bind函数返回0,则说明绑定成功;若返回值是-1,则可能因为各种原因而绑定失败,一般如果其他地方没什么问题,失败的原因很可能是指定的地址与其他套接字绑定了。下面是bind函数的应用示例:
/* 绑定指定的int sock套接字和指定的int port端口号 */ int BindServerSock(int sock, int port) { struct sockaddr_in sa; sa.sin_family = AF_INET; sa.sin_port = htons(port); sa.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr*)&sa, sizeof(sa)) < 0) return 0; return 1; }需要注意的是结构体sockaddr_in的sin_addr变量使用的是INADDR_ANY值,INADDR_ANY不是指定特定的值,而是任意值,以便欲连接到特定端口的任何Internet地址都能与对应的套接字进行连接。 使用bind函数成功将套接字与地址绑定后,需要设置套接字为等待客户端连接请求的状态,完成这一功能的函数就是listen()函数,listen函数的原型为:
int listen(int sock, int backlog);在该函数中需要特别留意的是参数backlog。当新的客户使用connect请求连接时,在连接请求被处理之前,将在backlog queue中等待,而backlog用于确定backlog queue的大小。假设该值设为10,则当未处理的连接用户大于10时,将产生backlog queue的溢出,此后请求连接的用户将出现错误,给系统网络带来严重问题。该值最好根据最大同时连接的客户端数量来指定backlog的值。backlog的值会受到系统最大值的限定,若大于系统最大值,则系统自动调整为系统最大值,所以不用担心太大。但是backlog的值不能为0,根据操作系统的不同,设置为0的处理方法可能不同,有可能只允许一个客户连接,也可能一个用户都不允许连接。
int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);accept返回值是新连接请求被处理结束后服务端与客户端进行通信的套接字的编号。当accept返回-1时,表示没有正常结束新的连接请求。一般accept有问题,很可能是整个网络功能有问题或者超出可创建套接字的最大数。详细的错误原因可以通过error获得。下面是accept的应用示例:
/* 接收新的客户端连接 */ int AcceptNewConnect(int sock) { int newSock; struct sockaddr_in per; socklen_t perSize; perSize = sizeof(per); newSock = accept(sock, (struct sockaddr*)&per,&perSize); if (newSock < 0) return -1; return newSock; }该函数的参数int sock是指定通过bind->listen函数处理,等待新连接的套接字,而返回值是与新连接进行数据交换的套接字编号,该函数失败时返回-1。
size_t send(int sock, const char* msg, size_t len, int flags);send函数的返回值是通过send函数发送数据的长度,当使用Nonblocking套接字时,有可能只发送比len指定长度小的数据。了解TCP/IP的数据传送形态就很容易理解出现这种情况的原因。在TCP/IP中,通过send函数发送的数据并不是直接发送到对方主机,而是先存储到缓冲器(严格来讲,可看做是通过TCP/IP4层协议的过程)后传送。如果系统的缓冲器没有剩余空间,或因其他原因,当调用send函数时不能保存msg变量的内容,则Blocking套接字等到处理结束,而Nonblocking套接字完成可能的处理后(或者没有完成处理本身)返回错误代码。如果在不了解这样的特性的情况下编写套接字应用程序,则当系统处于客户爆满或者系统网络处于超负荷等原因不能正常的数据交换时,就会碰到很多莫名其妙的bug。虽然在目前的高配置硬件环境下,这样的现象很少见,但最好还是考虑进去。如果send返回小于0的值,意味着发送数据的套接字出了问题,需要通过error函数检测错误代码。下面是send的应用示例:
/* 通过send向对方主机发送数据 */ int SendData(int sock, const char* buf, int size) { int sendSize; sendSize = send(sock, buf, size, 0); if (sendSize == 0) return -1; //当出现nonblocking模式的错误时 #ifdef EAGAIN if (error == EAGAIN) return 0; #endif // EAGAIN #ifdef EWOULDBLOCK if (ERROR == EWOULDBLOCK) return 0; #endif // EWOULDBLOCK //如果传送的数据和实际需要传送的数据不一致,则发送错误 if ((size - sendSize) != 0) return -1; return sendSize; }(切记随手保存啊、、、、这一段写了俩次,哭了
int recv(int s, void* buf, size_t len, int flags);recv返回值是从系统recv queue中取出并存储在buf变量的数据字节数,当返回0时,意味着对方主机正常断开了连接。当recv函数返回值小于0时,一位置相关套接字有错误。当使用Nonblocking套接字时,如果error的值为EWOULDBLOCK或者EAGAIN,则意味着并不是套接字本身有错,而是虽然调用了recv函数,但由于系统的recv queue空,没有可取出的数据,因此,直接返回recv函数。通过recv函数的第三个参数len传递的从系统recv queue取出并存储在buf变量的数据长度总是最大值。最大值的意思是,当实际系统的recv queue中有10字节数据时,即使设置len参数为1024,recv函数只取出10个字节数据,并返回值是10。与此相反,当实际系统的recv queue中有1024字节数据时,如果设置len参数为10,则只取出10个字节数据存储在buf变量中,而1014个字节数据仍然留在系统recv queue中。下面是recv函数的应用示例:
/* 数据的接收 */ int RecvData(int sock, char* recv_buf, int size) { int recvSize; recvSize = recv(sock, recv_buf, size, 0); if (recvSize > 0) return recvSize; if (recvSize == 0) return -1; //如果recvSize小于0 #ifdef EINTR if (ERROR == EINTR) return 0; #endif // EINTR #ifdef EAGAIN if (error == EAGAIN) return 0; #endif // EAGAIN #ifdef EWOULDBLOCK if (ERROR == EWOULDBLOCK) return 0; #endif // EWOULDBLOCK return -1; }在Nonblocking模式的套接字中,使用recv函数时,应该注意如下内容:
send(sock, "abcd", 4, 0);在使用send发送“abcd”4个字节的字符串到对方主机时,如果想的简单,则会认为在接收数据的主机方,调用一次recv函数,就获取4个字节的“abcd”字符串。但实际上,recv并不是这样工作的,这就给编程带来麻烦。明明是传送了4个字节的字符串,但作为调用recv函数的主机是无法知道以什么顺序传输了多少个字节的数据的。作为接收的一方,可以调用一次recv函数获取4个字节的字符串;也可以调用俩次recv函数获取字符串。因此,作为调用recv函数的接收方,很难知道接收的数据时以什么形式传输过来的。为了屏蔽这样的特性,一般的做法是在网络应用程序内部设置能保存recv的数据的队列或其他数据结构的缓冲器,并把recv数据有序的存储在这一缓冲器中,然后,以被存储的数据为基准,进行分析和使用。采用这样的方式,才可以进行完整的数据分析,也能防止数据的丢失。
int write(int sock, void* buf, size_t len);其参数与send函数的参数意义一致。
int read(int sock, void* buf, size_t len);
int sendto(int s, const char * msg, int len, int flags, const struct sockaddr FAR *to, socket_t tolen); int recvfrom(int s, void *buf, int len, int flags, struct sockaddr *from, socket_t *fromlen);在sendto和recvfrom俩个函数参数中,除第5个和第6个参数之外,其他参数的意义与send、recv函数的参数相同。但是需要注意的地方是,与面向连接的TCP不同,UDP调用各个函数时数据传输是独立处理的。sendto不经过系统send queue,直接进行传输处理的。当通过sendto函数按照A,B,C,D顺序传输数据报,并且利用recvfrom函数接收数据时,有可能按发送顺序接收数据,也有可能与发送顺序无关的接收数据(在利用send,recv函数的TCP套接字网络应用程序中,不管怎么样,当通过一个套接字传输数据时,接收方案send函数的发送顺序recv数据)。
热门源码