UNIX套接字编程

又回头看来一次socket的知识点,简单做个总结。有什么问题欢迎指出。顺便打个广告--游戏开发讨论群:462686014吐舌头吐舌头 

在UNIX中,创建套接字时和文件打开一样,在描述符表中取回一个int类型的索引号。套接字和文件是共享描述符表,因此他们的索引号不能重复,一个进程能同时创建最大的套接字数和文件数是相同的。

下面介绍开发中使用的函数。

1.套接字的创建、关闭

创建套接字时使用的函数是 socket(),其原型如下(注:函数参数详细介绍就不多赘述了吐舌头
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);

2.Blocking和Nonblocking

在套接字编程中,有“Blocking mode socket”和“Nonblocking mode socket”俩种模式,这俩种套接字模式的区别就是:当用创建的套接字进行处理时,比如调用特定的套接字函数进行数据发送或者接收时,等还是不等改函数的结果。
在以Blocking方式创建的套接字中,保存有对方主机传送的数据时。我们把该数据取到程序变量所使用的函数recv为例,若有10个字节数据可取,则使用recv函数时,取完10个字节后,返回recv函数。但如果没有recv函数可取的数据,则recv函数一直等待可取数据,不返回。
与此相反,若以Nonblocking方式创建套接字,则调用recv函数时,若有可取的数据,则取到程序的缓冲器中,若没有可取的数据,则返回一个错误信息。实际中,利用socket创建的套接字是Blocking模式,若想转换成Noblocking模式,需要进行下面的处理:
/*
设置套接字为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函数。

3.与其他主机进行连接

在OSI协议层的分组交换网的网络协议中,采用“两次握手”方式建立连接。在这种方式下,接受连接请求的主机发出同意连接应答,就可以实现连接,即关于建立连接的报文双方只传一次就可以了。
在TCP/IP中,建立连接的方式不是“两次握手”,而是“三次握手”。在“两次握手”方式中,建立连接的双方只需要进行俩次数据交换就可以建立连接,但“三次握手”中,需要进行三次数据交换,才可以建立连接。
与其他主机建立连接的函数是connect(),connect的函数原型如下:
int connect(int s, const struct sockaddr *name, int namelen);
当connect函数的调用成功时,即与对方主机连接 成功时,函数返回值为0,其他情况返回-1,若需要查看详细错误原因,可以利用errno函数检查。
下面是connect函数的应用示例。
/*
利用指定的参数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;
}

4.等待连接

客户端使用socket创建套接字,再利用connect与其他主机建立连接。而作为接受请求的主机服务器,在使用socket创建套接字后,需要绑定创建的套接字和客户端将请求连接的地址,负责套接字和地址绑定的函数是bind()函数,bind函数的原型如下:
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的处理方法可能不同,有可能只允许一个客户连接,也可能一个用户都不允许连接。

5.接受连接

当客户端发起连接请求并在backlog queue中等待时,就要处理接受连接的请求,并且获取与客户端交换数据的套接字,完成该功能的函数就是accept()。accept()函数的原型如下:
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。

6.数据传送

send
send()是通过套接字给对方主机发送数据的函数,该函数的原型如下所示:
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;
}
(切记随手保存啊、、、、这一段写了俩次,哭了 大哭 大哭
recv
recv是当对方主机通过send函数传输的数据保存到系统recv的队列时,从系统缓冲器中取出数据并存储到程序变量的函数,recv函数的原型如下所示。
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数据有序的存储在这一缓冲器中,然后,以被存储的数据为基准,进行分析和使用。采用这样的方式,才可以进行完整的数据分析,也能防止数据的丢失。
write
由于在UNIX操作系统中,可以像文件描述符的处理方式一样处理套接字,所以在套接字的处理中,也可以使用write函数。在套接字处理中,write函数的与本质上与send函数的用途相同。write函数的原型如下:
int write(int sock, void* buf, size_t len);
其参数与send函数的参数意义一致。
read
send函数和recv函数的关系与write函数和read函数的关系相同,即有相同的功能。read函数的原型如下:
int read(int sock, void* buf, size_t len);
sendto、recvfrom
sendto和recvfrom函数可以称为UDP函数。与TCP函数不同,UDP函数是没有必要再创建套接字后调用connect函数,在服务器中也没必要调用accept函数,直接可以进行下一步处理的函数。如果把sendto是函数connect和send函数的合并,recvfrom当做accept和recv函数的合并,及容易理解这俩个函数了。这俩个函数的原型如下:
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数据)。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值