设置和获得套接口选项
获得套接口选项
int getsockopt ( int sockfd, int level, int optname, void * optval, socklen_t *opteln )
设置套接口选项:
int setsockopt ( int sockfd, int level, int optname, const void * optval, socklen_t *opteln )
参数含意
sockfd(套接字): 指向一个打开的套接口描述字
level:(级别): 指定选项代码的类型。
SOL_SOCKET: 基本套接口
IPPROTO_IP: IPv4套接口
IPPROTO_IPV6: IPv6套接口
IPPROTO_TCP: TCP套接口
optname(选项名): 选项名称
optval(选项值): 是一个指向变量的指针 类型:整形,套接口结构, 其他结构类型:linger{}, timeval{ }
optlen(选项长度) :optval 的大小
返回值:标志打开或关闭某个特征的二进制选项
用于设置SOCKET细节
SO_REUSEADDR 重用地址
当打开某一端口的程序非正常退出,可能端口仍被占用,第二次执行程序就会报”Addr in use”无法使用这一端口
用SO_REUSEADDR 可以防止这一问题.服务器的socket,最好用这一选项.
n = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
val = 1;
setsockopt(n, SOL_SOCKET, SO_REUSEADDR, (char *) &val, sizeof (val));
// some code ...
if ((bind(n, (struct sockaddr *) &sin, sizeof (sin)) < 0)
|| (listen(n, QLEN) < 0))
exit(1);
SO_BROADCAST UDP广播选项
一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性
用sendto发送时,广播地址可以写
from.sin_addr.s_addr=INADDR_BROADCAST;
或是根据IP地址和掩码算出的子网的广播地址
int bBroadcast=1; setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(int));
UDP广播实例
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <errno.h>
#include <stdlib.h>
#include <arpa/inet.h>
int main(int argc, char **argv)
{
struct sockaddr_in s_addr;
int sock;
int addr_len;
int len;
char buff[128];
int yes;
/* 创建 socket */
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(errno);
} else
printf("create socket./n/r");
/* 设置通讯方式对广播,即本程序发送的一个消息,网络上所有主机均可以收到 */
yes = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(yes));
/* 唯一变化就是这一点了 */
/* 设置对方地址和端口信息 */
s_addr.sin_family = AF_INET;
if (argv[2])
s_addr.sin_port = htons(atoi(argv[2]));
else
s_addr.sin_port = htons(7838);
//Windows
if (argv[1])
s_addr.sin_addr.s_addr = inet_addr(argv[1]);
else {
printf("消息必须有一个接收者!/n");
exit(0);
}
/* 发送UDP消息 */
addr_len = sizeof(s_addr);
strcpy(buff, "hello i'm here");
len = sendto(sock, buff, strlen(buff), 0,
(struct sockaddr *) &s_addr, addr_len);
if (len < 0) {
printf("/n/rsend error./n/r");
return 3;
}
printf("send success./n/r");
return 0;
}
文件下载
文件下载一般采用TCP进行设计,这样的优点程序设计相对简单,防止文件内容在下载时丢失.
假设设计是从服务器下载一个文件到客户端,在TCP打开时,服务器首先去读文件信息,如文件名,文件长度等,将其写入一个指定长度的结构头里,发往客户端,然后将文件内容依次发往客户端.
客户端首先收下定长的结构体,从中读取文件名和文件长度.然后连续接收文件长度的字符,即可将文件完整接收下来.
由于TCP是不会丢失数据,因此只有二种可能,一种是接收失败,一种是把文件完整接收下来.
文件的阻塞函数
一般文件读取函数read,socket的接收函数recv,recvfrom都是阻塞型函数,即没有数据收到时,整个程序被阻塞这个函数
在服务器软件上,经常要接收多个客户端的数据.如果单纯采用recv造成整个程序的阻塞.
一种方法采用多线程.
更为常用是采用多路复用函数select来同时控制多个socket/file 描述符.
select多路复用
select系统调用是用来让我们的程序监视多个文件句柄(file descrīptor)的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有某一个或多个发生了状态改变。
普通文件读写
socket的收发
设备文件的收发
fd_set数据结构
select主要操作fd_set的数据结构.fd_set是一个文件描述符的矢量数组
大体上可以把fd_set看成一个只有32项的整数数组.
每一个socket或fd都是fd_set中的一项,
一般采用一组宏来操作fd_set
void FD_SET(int fd,fd_set *fdset)
void FD_CLR(int fd,fd_set *fdset)
FD_CLR将fd从fdset里面清除
void FD_ZERO(fd_set *fdset)
FD_ZERO从fdset中清除所有的文件描述符
int FD_ISSET(int fd,fd_set *fdset)
FD_ISSET判断fd是否在fdset集合中
typedef struct fd_set { u_int fd_count; int fd_array[fd_setsize]; }
select定义
int select(int max_fd, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
其中max_fd为我们要监听的套接字中值最大的一个,同时在调用select是要将其加1,
readfd即为我们监听的要进行读操作的套接字连接集合,
第三个参数是我们监听的要进行写操作的套接字连接集合,
第四个参数用于异常,而最后一个参数可以用来设定超时,这里同样使用了struct timeval结构,
当有文件被写时,返回一个大于0值,出错返回一个负数,等于表示在timeout的时间,没有任何读写,select是超时返回的
select()的使用
select同时监控多个激活的socket(最大值一般为1024)
当相应的socket上有数据接收时,select将其值写入readfd值中.并返回一个大于0值.
这样通过FD_ISSET可以查出是哪一个socket被读写.因为只有一个阻塞点.大大提高程序的性能
因为带有超时机制,也能防止长时间阻塞导致程序无法响应的后果
Select也能处理一般的文件或设备文件,如把标准输入或普通文件加入到监控的集合中
select实例
int main() { int ret; fd_set fds; struct timeval tv; FD_ZERO(&fds); FD_SET(0,&fds);//把标准输入加入监控
tv.tv_sec = 5;
tv.tv_usec = 0;
ret = select(1, &fds, NULL, NULL, &tv);
if(ret < 0)
{ perror(“select”); exit(-1)
}else if(ret == 0)
{//5 秒钟内用户没有按下键
printf(“timeout”);
} else
{ // 读入用户输入 scanf("%s", buf);
} }
Select的socket下使用流程
Select 的使用是固定的流程
socket(...); bind(...); listen(...); while(1) { FD_ZERO(…) FD_SET(…) select(…); // 如果是服务器侦听套接字被触发,说明一个新的连接请求建立
if(FD_ISSET(svr_fd,…)) { // 建立新的客户联接连接
new_fd = accept(…) ;
// 加入到监听文件描述符中去;
FD_SET(new_fd,…) } else
{
// 是一个客户端操作
进行操作(read或者write); } }
练习
请将原有简单文件下载服务器,由一次下载一个文件,升级为一次可下多个文件的.
多个文件名可由配置文件或直接写在源码里,请将广播和聊天服务器代码移植到Linux上