目录
网络服务器编程模型
函数名称 | 函数简单描述 | 附加说明 |
---|---|---|
socket | 创造某种类型的套接字 | |
bind | 将一个 socket | 绑定一个ip与端口的二元组上 |
listen | 将一个 socket 变为侦听状态 | |
connect | 试图建立一个 TCP 连接 | 一般用于客户端 |
accept | 尝试接收一个连接 | 一般用于服务端 |
send | 通过一个socket发送数据 | |
recv | 通过一个socket收取数据 | |
select | 判断一组socket上的读事件 | |
gethostbyname | 通过域名获取机器地址 | |
close | 关闭一个套接字,回收该 socket 对应的资源 | Windows 系统中对应的是 closesocket |
shutdown | 关闭 socket 收或发通道 | |
setsockopt | 设置一个套接字选项 | |
getsockopt | 获取一个套接字选项 |
socket
socket()函数的原型如下,这个函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- domain
函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义。
名称 | 含义 | 名称 | 含义 |
---|---|---|---|
PF_UNIX PF_LOCAL | 本地通信 | PF_X25 | ITU-T X25 / ISO-8208协议 |
AF_INET,PF_INET | IPv4 Internet协议 | PF_AX25 | Amateur radio AX.25 |
PF_INET6 | IPv6 Internet协议 | PF_ATMPVC | 原始ATM PVC访问 |
PF_IPX | IPX-Novell协议 | PF_APPLETALK | Appletalk |
PF_NETLINK | 内核用户界面设备 | PF_PACKET | 底层包访问 |
- type
type 函数socket()的参数type用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字)、SOCK——DGRAM(数据包套接字)等。
名称 | 含义 |
---|---|
SOCK_STREAM | Tcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输 |
SOCK_DGRAM | 支持UDP连接(无连接状态的消息) |
SOCK_SEQPACKET | 序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出 |
SOCK_RAW | RAW类型,提供原始网络协议访问 |
SOCK_RDM | 提供可靠的数据报文,不过可能数据会有乱序 |
并不是所有的协议族都实现了这些协议类型,例如,AF_INET协议族就没有实现SOCK_SEQPACKET协议类型。
- protocol
函数socket()的第3个参数protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。
- errno
函数socket()并不总是执行成功,有可能会出现错误,错误的产生有多种原因,可以通过errno获得:
值 | 含义 |
---|---|
EACCES | 没有权限建立制定的domain的type的socket |
EAFNOSUPPORT | 不支持所给的地址类型 |
EINVAL | 不支持此协议或者协议不可用 |
EMFILE | 进程文件表溢出 |
ENFILE | 已经达到系统允许打开的文件数量,打开文件过多 |
ENOBUFS/ENOMEM | 内存不足。socket只有到资源足够或者有进程释放内存 |
EPROTONOSUPPORT | 制定的协议type在domain中不存在 |
比如我们建立一个流式套接字可以这样:
int sock = socket(AF_INET, SOCK_STREAM, 0);
bind
在套接口中,一个套接字只是用户程序与内核交互信息的枢纽,它自身没有太多的信息,也没有网络协议地址和 端口号等信息,在进行网络通信的时候,必须把一个套接字与一个地址相关联,这个过程就是地址绑定的过程。许多时候内核会我们自动绑定一个地址,然而有时用 户可能需要自己来完成这个绑定的过程,以满足实际应用的需要,最典型的情况是一个服务器进程需要绑定一个众所周知的地址或端口以等待客户来连接。这个事由 bind的函数完成。
int bind( int sockfd, struct sockaddr* addr, socklen_t addrlen)
- sockfd 就是我们调用socket函数后创建的socket 句柄或者称文件描述符号。
- addr addr是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要弦将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等接合在一起。
由于历史原因,我们前后有两个地址结构: struct sockaddr 该结构定义如下:
struct sockaddr {
uint8_t sa_len;
unsigned short sa_family; /* 地址家族, AF_xxx */
char sa_data[14]; /*14字节协议地址*/
};
其实这个结构逐渐被舍弃,但是也还是因为历史原因,在很多的函数,比如connect、bind等还是用这个作为声明,实际上现在用的是第二个结构,我们需要把第二个结构强转成sockaddr。 struct sockaddr_in 其定义如下:
struct sockaddr_in {
uint8_t sa_len; /* 结构体长度*/
short int sin_family; /* 通信类型 */
unsigned short int sin_port; /* 端口 */
struct in_addr sin_addr; /* Internet 地址 */
unsigned char sin_zero[8]; /* 未使用的*/
};
struct in_addr { //sin_addr的结构体类型in_addr 原型
unsigned long s_addr; /*存4字节的 IP 地址(使用网络字节顺序)。*/
};
在使用的时候我们必须指定通信类型,也必须把端口号和地址转换成网络序的字节序
- addrlen addr结构的长度,可以设置成sizeof(struct sockaddr)。使用sizeof(struct sockaddr)来设置套接字的类型和其对已ing的结构。
bind()函数的返回值为0时表示绑定成功,-1表示绑定失败,errno的错误值如表1所示。
值 | 含义 | 备注 |
---|---|---|
EADDRINUSE | 给定地址已经使用 | |
EBADF | sockfd不合法 | |
EINVAL | sockfd已经绑定到其他地址 | |
ENOTSOCK | sockfd是一个文件描述符,不是socket描述符 | |
EACCES | 地址被保护,用户的权限不足 | |
EADDRNOTAVAIL | 接口不存在或者绑定地址不是本地 | UNIX协议族,AF_UNIX |
EFAULT | my_addr指针超出用户空间 | UNIX协议族,AF_UNIX |
EINVAL | 地址长度错误,或者socket不是AF_UNIX族 | UNIX协议族,AF_UNIX |
ELOOP | 解析my_addr时符号链接过多 | UNIX协议族,AF_UNIX |
ENAMETOOLONG | my_addr过长 | UNIX协议族,AF_UNIX |
ENOENT | 文件不存在 | UNIX协议族,AF_UNIX |
ENOMEN | 内存内核不足 | UNIX协议族,AF_UNIX |
ENOTDIR | 不是目录 | UNIX协议族,AF_UNIX |
比如这样:
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0)
{
perror("bind");
exit(1);
}
listen
int listen(int sockfd, int backlog);
listen()函数将sockfd标记为被动打开的套接字,并作为accept的参数用来接收到达的连接请求。
- sockfd是一个套接字类型的文件描述符,具体类型为SOCK_STREAM或者SOCK_SEQPACKET。
- backlog参数用来描述sockfd的等待连接队列能够达到的最大值。当一个请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,或者如果底层协议支持重传(比如tcp协议),本次请求会被丢弃不作处理,在下次重试时期望能连接成功(下次重传的时候队列可能已经腾出空间)。
说起这个backlog就有一点儿历史了,等下文描述。
- errno
值 | 含义 |
---|---|
EADDRINUSE | 另一个套接字已经绑定在相同的端口上。 |
EBADF | 参数sockfd不是有效的文件描述符。 |
ENOTSOCK | 参数sockfd不是套接字。 |
EOPNOTSUPP | 参数sockfd不是支持listen操作的套接字类型。 |
connect
声明如下
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明如下
- sockfd 是系统调用 socket() 返回的套接字文件描述符。
- serv_addr 是 保存着目的地端口和 IP 地址的数据结构 struct sockaddr_in。
- addrlen 设置 为 sizeof(struct sockaddr_in)
errno
connect函数在调用失败的时候返回值-1,并会设置全局错误变量 errno。
值 | 含义 |
---|---|
EBADF | 参数sockfd 非合法socket处理代码 |
EFAULT | 参数serv_addr指针指向无法存取的内存空间 |
ENOTSOCK | 参数sockfd为一文件描述词,非socket。 |
EISCONN | 参数sockfd的socket已是连线状态 |
ECONNREFUSED | 连线要求被server端拒绝。 |
ETIMEDOUT | 企图连线的操作超过限定时间仍未有响应。 |
ENETUNREACH | 无法传送数据包至指定的主机。 |
EAFNOSUPPORT | sockaddr结构的sa_family不正确。 |
accept
函数声明
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明
sockfd是由socket函数返回的套接字描述符,参数addr和addrlen用来返回已连接的对端进程(客户端)的协议地址。如果我们对客户端的协议地址不感兴趣,可以把arrd和addrlen均置为空指针。
返回值
成功时,返回非负整数,该整数是接收到套接字的描述符;出错时,返回-1,相应地设定全局变量errno。
值 | 含义 |
---|---|
EBADF | 非法的socket |
EFAULT | 参数addr指针指向无法存取的内存空间 |
ENOTSOCK | 参数s为一文件描述词,非socket |
EOPNOTSUPP | 指定的socket并非SOCK_STREAM |
EPERM | 防火墙拒绝此连线 |
ENOBUFS | 系统的缓冲内存不足 |
ENOMEM | 核心内存不足 |
特别需要说明下的是,这个accept是一个阻塞式的函数,对于一个阻塞的套套接字,一直阻塞,或者返回一个错误值,对于非阻塞套接字。accept有可能返回-1,但是如果errno的值为,EAGAIN或者EWOULDBLOCK,此时需要重新调用一次accept函数。
send_recv send和recv函数
函数申明如下
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- sockfd :套接字
- buf : 待发送或者接收的缓存
- len : 如果是recv指期望接收的长度,如果是send指要发送的长度。
- flags : 标志位,取值如下表:
flags | 说明 | recv | send |
---|---|---|---|
MSG_DONTROUTE | 绕过路由表查找 | • | |
MSG_DONTWAIT | 仅本操作非阻塞 | • | • |
MSG_OOB | 发送或接收带外数据 | • | • |
MSG_PEEK | 窥看外来消息 | • | |
MSG_WAITALL | 等待所有数据 | • |
errno
值 | 含义 |
---|---|
EAGAIN | 套接字已标记为非阻塞,而接收操作被阻塞或者接收超时 |
EBADF | sock不是有效的描述词 |
ECONNREFUSE | 远程主机阻绝网络连接 |
EFAULT | 内存空间访问出错 |
EINTR | 操作被信号中断 |
EINVAL | 参数无效 |
ENOMEM | 内存不足 |
ENOTCONN | 与面向连接关联的套接字尚未被连接上 |
ENOTSOCK | sock索引的不是套接字 当返回值是0时,为正常关闭连接; |
当返回值为-1时是不是一定就错误了,当返回值为0时该怎么做呢?
如何正确判断一个对端已经关闭了连接?
/*客户端设置非阻塞,然后判断链接是否成功*/
int SocketConnectWithTimeout
(
int mySocket,
struct mySocketaddr *adrs,
int adrsLen,
struct timeval *timeVal
)
{
int flag;
fd_set writeFds;
int remotPeerAdressLen;
struct mySocketaddr remotPeerAdress;
if(timeVal == NULL)
{
return (connect(mymySocket, adrs, adrsLen));
}
flag = fcntl(mySocket, F_GETFL, 0);
fcntl(mySocket, F_SETFL, flag | O_NONBLOCK);//修改当前的flag标志为给阻塞
//对于非阻塞式套接字,如果调用connect函数会之间返回-1(表示出错),且错误为EINPROGRESS,表示连接建立
if(connect(mySocket, adrs, adrsLen) < 0)
{
//当使用非阻塞模式的时候,如果链接没有被立马建立,则connect()返回EINPROGRESS
if(errno == EINPROGRESS)
{
//select是一种IO多路复用机制,它允许进程指示内核等待多个事件的任何一个发生,并且在有一个或者多个事件发生或者经历一段指定的时间后才唤醒它。
//connect本身并不具有设置超时功能,如果想对套接字的IO操作设置超时,可使用select函数。此时我们使用不断的检测writeFds来判断链接的建立?
FD_ZERO(&writeFds);
FD_SET((unsigned int)mySocket, &writeFds);
if(select(FD_SETSIZE, (fd_set *)NULL, &writeFds, (fd_set *)NULL, timeVal) > 0)
{
//select()成功了,查看mySocketet是否可写(关键)
if (FD_ISSET ((unsigned int)mySocket, &writeFds))
{
//已经可写了,此时我们要通过使用getpeername()判断是否真正的链接成功,如果返回值不是-1;
//说明connect()成功了。
remotPeerAdressLen = sizeof (remotPeerAdress);
if(getpeername (mySocket, &remotPeerAdress, &remotPeerAdressLen) != ERROR)
{
return OK;
}
else
{
return ERROR;
}
}
}
}
else
{
return ERROR;
}
}
fcntl(mySocket, F_SETFL, flag);//恢复标志位为阻塞
}
1.将打开的socket设为非阻塞的,可以用fcntl(socket, F_SETFL, O_NDELAY)完成(有的系统用FNEDLAY也可).
2.发connect调用,这时返回-1,但是errno被设为EINPROGRESS,意即connect仍旧行还没有完成.
3.将打开的socket设进被监视的可写(注意不是可读)文件集合用select进行监视,如果可写用getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int)); 来得到error的值,如果为零,则connect成功.
服务器例子和客户端例子
服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define Port 6666 //端口号
#define MAXCLIENT 10 //最大客户端数量
int main(int argc, char argv[])
{
int socket_fd, client_fd;
int ret;
int addr_size;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int read_size;
char buffer[1024];
//创建socket
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if( socket_fd == -1)
{
printf("socket error\n");
exit(1);
}
//绑定bind
bzero(&server_addr, sizeof(struct sockaddr_in));//清空数据
server_addr.sin_family = AF_INET;//IPv4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//将主机IP转换为网络IP
server_addr.sin_port = htons(Port);//将主机端口转换为网络Port
ret = bind(socket_fd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr));
if(ret == -1)
{
printf("bind error\n");
exit(1);
}
//监听
ret = listen(socket_fd, MAXCLIENT);
if(ret == -1)
{
printf("listen error\n");
exit(1);
}
while(1)
{
//accept
addr_size = sizeof(struct sockaddr_in);
client_fd = accept(socket_fd, (struct sockaddr *)(&client_addr), &addr_size);
if(client_fd == -1)
{
printf("accept error\n");
exit(1);
}
//打印客户端IP 将网络地址转换成 .字符串
printf("Server get connection from %s\n",inet_ntoa(client_addr.sin_addr));
if((read_size = read(client_fd, buffer, 1024)) == -1)
{
printf("Read Error\n");
exit(1);
}
buffer[read_size]='\0';
printf("Server received %s\n",buffer);
close(client_fd); /* 循环下一个 */
}
close(socket_fd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define Port 6666
int main(int argc, char argv[])
{
int socket_fd;
int ret;
char buff[1024];
struct sockaddr_in server_addr;
char* str_IP = "172.21.252.7";
//创建客户端socket
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if( socket_fd == -1)
{
printf("socket error\n");
exit(1);
}
//连接connect
bzero(&server_addr, sizeof(struct sockaddr_in));//清空数据
server_addr.sin_family = AF_INET;//IPv4
server_addr.sin_addr.s_addr = inet_addr(str_IP);//将主机IP转换为网络IP
server_addr.sin_port = htons(Port);//将主机端口转换为网络Port
ret = connect(socket_fd, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr_in));
if(ret == -1)
{
printf("connect error\n");
exit(1);
}
while(1)
{
//连接成功了,发送数据
printf("Please input char:\n");
fgets(buff, 1024, stdin);
write(socket_fd, buff, strlen(buff));
}
close(socket_fd);
return 0;
}