1服务端的初始化
下面是一个tcp和udp的server端初始化的通用封装函数,来自于APUE。非常的好用。
其包括创建套接字,bind ,listen(udp省去)
/********************************************************************
* 函数名: initserver
* 功 能: 初始化网络服务器,包括创建套接字,listen, bind等
* 版本 : come from APUE
********************************************************************/
int
initserver(int type, const struct sockaddr *addr, socklen_t alen,int qlen)
{
int fd, err;
int reuse = 1;
if ((fd = socket(addr->sa_family, type, 0)) < 0)
return(-1);
/* 使得重复使用本地地址与套接字进行绑定 */
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse,
sizeof(int)) < 0) {
err = errno;
goto errout;
}
if (bind(fd, addr, alen) < 0) {
err = errno;
goto errout;
}
//如果是udp的server则不需要用到listen
if (type == SOCK_STREAM || type == SOCK_SEQPACKET) {
if (listen(fd, qlen) < 0) {
err = errno;
goto errout;
}
}
return(fd);
errout:
close(fd);
errno = err;
return(-1);
}
例如TCP的服务端可以这样调用:
struct sockaddr_in serv_addr, client_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(NET_PORT); //18020
serv_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(serv_addr.sin_zero), 8);
/*建立socket连接*/
if( (socketd = initserver(SOCK_STREAM, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr),10)) >= 0 )
{
Accept().......
Receive()..........
}
Tcp与udp的编程区别
Tcp与udp的区别就是前者必须在服务端与客户端建立连接的基础上进行,即其server端必须有accept,而其client端必须connect主机。这样链接后双方便可用rev(),send()函数了,不一定要用recvfrom(),sendto(),因为双方的地址都是知道了的。而UDP则相反,在通信之前无需建立连接,所以要用recvfrom(),sendto(),其中的参数指定对方的地址。
下面总结下网络编程中值得注意的地方:
1数据存储顺序的转换:(h:host,n:network,s:short)
Htons(),ntohs(),htonl,ntohl()
如在设置端口时
serv_addr.sin_port = htons(18020);
必须把端口号18020转换成网络数据格式(大端),否则出错。我们无需知道主机的字节序,因为系统会有相关的宏去判断,确定htons()函数中是否真的去进行转换了。
2地址格式的转换:
1)Ip地址点分十进制字符串(“用户习惯的如“192.168.14.34”)与二进制转换(网络传输使用的)函数:
Ipv4:inet_aton(),inet_ntoa(),inet_addr()
Ipv4,ipv6兼容:inet_pton(),inet_ntop()
个人喜欢用IP4的前两个函数,调用的参数方便些。
2)物理地址字符串格式与网络二进制格式转换
ether_ntoa()
3几个字符串格式地址与数组存储格式的转换函数(自己写的);
/********************************************************************
* 函数名: ip_to_str
* 参数名: char *ip_str:(in)
unsigned char *ip_addr:(out)
*
* 返回值: no
* 功 能: 点十进制的字符串转成ip数组
* 版本 : water-2010-07-08--first
********************************************************************/
void str_to_ip(unsigned char *ip_str, unsigned char *ip_addr)
{
unsigned char tmp;
unsigned char *ip = ip_str;
while((*ip) != '/0')
{
tmp = 0;
while((*ip) != '.' && (*ip) != '/0')
{
tmp *= 10;
tmp += *ip -'0';
ip++;
}
if ( (*ip) == '.')
{
ip++;
*ip_addr = tmp;
ip_addr++;
}
}
*ip_addr = tmp;
}
/********************************************************************
* 函数名: str_to_mac
* 参数名: char *mac_str :(in)
unsigned char *mac_addr :(out)
*
* 返回值: no
* 功 能: 冒号16进制的字符串转成mac数组
* 版本 : water-2010-07-08--first
********************************************************************/
void str_to_mac(unsigned char *mac_str, unsigned char *mac_addr)
{
unsigned char tmp;
unsigned char *mac = mac_str;
while((*mac) != '/0')
{
tmp = 0;
while((*mac) != ':' && (*mac) != '/0')
{
tmp *= 16;
if(*mac >='0' && *mac <= '9')
tmp += *mac -'0';
if(*mac >= 'a' && *mac <= 'f')
tmp = tmp + 10 + *mac - 'a';
if(*mac >= 'A' && *mac <= 'F')
tmp = tmp +10 +*mac - 'A';
mac++;
}
if ( (*mac) == ':')
{
mac++;
*mac_addr = tmp;
mac_addr++;
}
}
*mac_addr = tmp;
}
/********************************************************************
* 函数名: ip_to_str
* 参数名: char *src :(in)
unsigned char *ip_str :(out)
*
* 返回值: no
* 功 能: ip数组转点十进制字符串
* 版本 : water-2010-07-08--first
********************************************************************/
void ip_to_str(unsigned char *ip_str, unsigned char *src)
{
sprintf(ip_str,"%d.%d.%d.%d",*(src), *(src+1), *(src+2), *(src+3));
}
/********************************************************************
* 函数名: mac_to_str
* 参数名: char *src :(in)
unsigned char *mac_str :(out)
*
* 返回值: no
* 功 能: mac数组转":16"进制字符串
* 版本 : water-2010-07-08--first
********************************************************************/
void mac_to_str(unsigned char *mac_str, unsigned char *src)
{
sprintf(mac_str,"%x:%x:%x:%x:%x:%x",*(src), *(src+1), *(src+2), *(src+3),*(src+4),*(src+5));
}
几个网络相关的地址的获得;
/********************************************************************
* 函数名: get_mac
* 参数名: char *mac_addr ( out)
*
* 返回值: 0 成功
* -1 失败
* 功 能: 获取本机物理地址,以字符串形式返回
* 版本 : water-2010-07-08--first
********************************************************************/
int get_mac(char *mac_addr)
{
int nSocket;
struct ifreq struReq;
if ( (nSocket = socket(PF_INET,SOCK_STREAM,0)) < 0)
{
error("socket");
return -1;
}
memset(&struReq,0,sizeof(struReq));
strncpy(struReq.ifr_name, "eth0", sizeof(struReq.ifr_name));
if( ioctl(nSocket,SIOCGIFHWADDR,&struReq) < 0)
{
perror("ioctl");
return -1;
}
close(nSocket);
memcpy(mac_addr,( char *)ether_ntoa(struReq.ifr_hwaddr.sa_data),strlen(( char*)ether_ntoa(struReq.ifr_hwaddr.sa_data))+1);
return 0;
}
/********************************************************************
* 函数名: get_ip
* 参数名: char *ip_addr ( out)
*
* 返回值: 0 成功
* -1 失败
* 功 能: 获取本机ip地址,以字符串形式返回
* 版本 : water-2010-07-08--first
********************************************************************/
int get_ip(char *ip_addr)
{
int sock;
struct sockaddr_in sin;
struct ifreq ifr;
if((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0 )
{
perror("socket");
return -1;
}
strncpy(ifr.ifr_name, "eth0", sizeof(ifr.ifr_name));
if (ioctl(sock, SIOCGIFADDR, &ifr) < 0)
{
perror("ioctl");
return -1;
}
memcpy(&sin, &ifr.ifr_addr, sizeof(sin));
close(sock);
memcpy(ip_addr,inet_ntoa(sin.sin_addr),strlen(inet_ntoa(sin.sin_addr))+1);
return 0;
}
//这里用一个函数来完成获得ip,netmask,mac.这个函数非常好用。
/********************************************************************
* 函数名: get_host_info
* 参数名: local_ip(out) my_ip
* local_mask(out) my_mask
local_mac(out) my_mac
* 返回值: 0 成功
* -1 失败
* 功 能:获取本地机的ip,掩码,物理地址
* 版本 : water-2010-07-08--first
********************************************************************/
int get_host_info(char *const local_ip, char *const local_mask, char *const local_mac)
{
struct sockaddr_in my_ip, my_mask, my_mac;
struct ifreq ifr;
int sock;
if((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("socket");
return -1;
}
memset(&ifr,0,sizeof(ifr));
strcpy(ifr.ifr_name, "eth0");
//取本机IP地址
if(ioctl(sock, SIOCGIFADDR, &ifr) < 0)
{
perror("ioctl_get_ip");
return -1;
}
memcpy(&my_ip, &ifr.ifr_addr, sizeof(my_ip));
memcpy( local_ip, inet_ntoa(my_ip.sin_addr), strlen( inet_ntoa(my_ip.sin_addr) ) +1);
//取本机掩码
if( ioctl( sock, SIOCGIFNETMASK, &ifr) == -1 )
{
perror("ioctl_get_mask");
return -1;
}
memcpy(&my_mask, &ifr.ifr_addr, sizeof(my_mask));
memcpy( local_mask, inet_ntoa(my_mask.sin_addr), strlen( inet_ntoa(my_mask.sin_addr) )+1 );
//取本机mac
if ( ioctl( sock,SIOCGIFHWADDR,&ifr) < 0 )
{
perror("ioctl_get_mac");
return -1;
}
memcpy(local_mac,(char *)ether_ntoa(ifr.ifr_hwaddr.sa_data),strlen((char*)ether_ntoa(ifr.ifr_hwaddr.sa_data))+1);
close(sock);
return 0;
}
//获取网关相当繁琐些,具体代码如下:
/********************************************************************
* 函数名: get_gateway
* 参数名: gateway(out) 网关
* 返回值: 0 成功
* -1 失败
* 功 能:获取本地机的网关
********************************************************************/
int get_gateway(char *gateway)
{
struct nlmsghdr *nlMsg;
struct rtmsg *rtMsg;
struct route_info *rtInfo;
char msgBuf[BUFSIZE];
int sock, len, msgSeq = 0;
//创建 Socket
if((sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)) < 0)
{
perror("Socket Creation: ");
return -1;
}
/* Initialize the buffer */
memset(msgBuf, 0, BUFSIZE);
/* point the header and the msg structure pointers into the buffer */
nlMsg = (struct nlmsghdr *)msgBuf;
rtMsg = (struct rtmsg *)NLMSG_DATA(nlMsg);
/* Fill in the nlmsg header*/
nlMsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); // Length of message.
nlMsg->nlmsg_type = RTM_GETROUTE; // Get the routes from kernel routing table .
nlMsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; // The message is a request for dump.
nlMsg->nlmsg_seq = msgSeq++; // Sequence of the message packet.
nlMsg->nlmsg_pid = getpid(); // PID of process sending the request.
/* Send the request */
if(send(sock, nlMsg, nlMsg->nlmsg_len, 0) < 0){
printf("Write To Socket Failed.../n");
return -1;
}
/* Read the response */
if((len = readNlSock(sock, msgBuf, msgSeq, getpid())) < 0) {
printf("Read From Socket Failed.../n");
return -1;
}
/* Parse and print the response */
rtInfo = (struct route_info *)malloc(sizeof(struct route_info));
for(;NLMSG_OK(nlMsg,len);nlMsg = NLMSG_NEXT(nlMsg,len)){
memset(rtInfo, 0, sizeof(struct route_info));
parseRoutes(nlMsg, rtInfo,gateway);
}
free(rtInfo);
close(sock);
return 0;
}
// the follow sources for get the gateway
//下面两个函数是获取网关是用到的
int readNlSock(int sockFd, char *bufPtr, int seqNum, int pId)
{
struct nlmsghdr *nlHdr;
int readLen = 0, msgLen = 0;
do{
//收到内核的应答
if((readLen = recv(sockFd, bufPtr, BUFSIZE - msgLen, 0)) < 0)
{
perror("SOCK READ: ");
return -1;
}
nlHdr = (struct nlmsghdr *)bufPtr;
//检查header是否有效
if((NLMSG_OK(nlHdr, readLen) == 0) || (nlHdr->nlmsg_type == NLMSG_ERROR))
{
perror("Error in recieved packet");
return -1;
}
/* Check if the its the last message */
if(nlHdr->nlmsg_type == NLMSG_DONE)
{
break;
}
else
{
/* Else move the pointer to buffer appropriately */
bufPtr += readLen;
msgLen += readLen;
}
/* Check if its a multi part message */
if((nlHdr->nlmsg_flags & NLM_F_MULTI) == 0)
{
/* return if its not */
break;
}
} while((nlHdr->nlmsg_seq != seqNum) || (nlHdr->nlmsg_pid != pId));
return msgLen;
}
//分析返回的路由信息
void parseRoutes(struct nlmsghdr *nlHdr, struct route_info *rtInfo,char *gateway)
{
struct rtmsg *rtMsg;
struct rtattr *rtAttr;
int rtLen;
char *tempBuf = NULL;
//2007-12-10
struct in_addr dst;
struct in_addr gate;
tempBuf = (char *)malloc(100);
rtMsg = (struct rtmsg *)NLMSG_DATA(nlHdr);
// If the route is not for AF_INET or does not belong to main routing table
//then return.
if((rtMsg->rtm_family != AF_INET) || (rtMsg->rtm_table != RT_TABLE_MAIN))
return;
/* get the rtattr field */
rtAttr = (struct rtattr *)RTM_RTA(rtMsg);
rtLen = RTM_PAYLOAD(nlHdr);
for(;RTA_OK(rtAttr,rtLen);rtAttr = RTA_NEXT(rtAttr,rtLen)){
switch(rtAttr->rta_type) {
case RTA_OIF:
if_indextoname(*(int *)RTA_DATA(rtAttr), rtInfo->ifName);
break;
case RTA_GATEWAY:
rtInfo->gateWay = *(u_int *)RTA_DATA(rtAttr);
break;
case RTA_PREFSRC:
rtInfo->srcAddr = *(u_int *)RTA_DATA(rtAttr);
break;
case RTA_DST:
rtInfo->dstAddr = *(u_int *)RTA_DATA(rtAttr);
break;
}
}
//2007-12-10
dst.s_addr = rtInfo->dstAddr;
if (strstr((char *)inet_ntoa(dst), "0.0.0.0"))
{
gate.s_addr = rtInfo->gateWay;
sprintf(gateway, (char *)inet_ntoa(gate));
}
free(tempBuf);
return;
}
这些参数的设置可以调用system()函数来,执行一些命令完成。如system(ifconfig eth0 192.168.14.34)等,也可以编写一些函数如下面这个:/********************************************************************
* 函数名: set_ip
* 参数名: char *ip (in)
*
* 返回值: 0 成功
* -1 失败
* 功 能: 设置本机ip地址 为char *ip所指定的字符串ip
* 版本 : water-2010-07-08--first
********************************************************************/
int set_ip(char *ip)
{
struct ifreq temp;
struct sockaddr_in *addr;
int fd = 0;
int ret = -1;
strcpy(temp.ifr_name, "eth0");
if((fd=socket(AF_INET, SOCK_STREAM, 0))<0)
{
perror("socket");
return -1;
}
addr = (struct sockaddr_in *)&(temp.ifr_addr);
addr->sin_family = AF_INET;
addr->sin_addr.s_addr = inet_addr(ip);
ret = ioctl(fd, SIOCSIFADDR, &temp);
close(fd);
if(ret < 0)
{
perror("ioctl");
return -1;
}
return 0;
}
然而这样系统重启后起IP一般会变回原来。因为一般系统启动后都会调用一个配置文件来设置相关网络地址。如PC上的linux一般会有/etc/sysconfig/network-scripts/ifcfg-eth0
而一般的嵌入式linux则不会有。所以在pc上我们可以修改这个配置文件,记得曾经在网上找到几个读取,修改该配置文件的几个函数,现在贴出来:
/********************************************************************
* 函数名: ReadConfig(char *conf_path,char *conf_name,char *config_buff)
* 参数名: char *conf_path
* char *conf_name
char *config_buff
* 返回值: no
* 功 能: 从配置文件中读取相应的值输入参数:1,配置文件路径 2,匹配标记 3,输出存储空间
* 并且排除了空行,“=”前后无内容,无“=”的情况
* 版本 : water-2010-07-08--first
********************************************************************/
int ReadConfig(char *conf_path,char *conf_name,char *config_buff)
{
char config_linebuf[256];
char line_name[40];
char exchange_buf[256];
char *config_sign = "=";
char *leave_line;
FILE *f;
f = fopen(conf_path,"r");
if(f == NULL)
{
printf("OPEN CONFIG FALID/n");
return 0;
}
fseek(f,0,SEEK_SET);
while(fgets(config_linebuf,256,f) != NULL)
{
if(strlen(config_linebuf) < 3) //判断是否是空行
{
continue;
}
if (config_linebuf[strlen(config_linebuf)-1] == 10) //去除最后一位是/n的情况
{
memset(exchange_buf,0,sizeof(exchange_buf));
strncpy(exchange_buf,config_linebuf,strlen(config_linebuf)-1);
memset(config_linebuf,0,sizeof(config_linebuf));
strcpy(config_linebuf,exchange_buf);
}
memset(line_name,0,sizeof(line_name));
leave_line = strstr(config_linebuf,config_sign);
if(leave_line == NULL) //去除无"="的情况
{
continue;
}
int leave_num = leave_line - config_linebuf;
strncpy(line_name,config_linebuf,leave_num);
if(strcmp(line_name,conf_name) ==0)
{
strncpy(config_buff,config_linebuf+(leave_num+1),strlen(config_linebuf)-leave_num-1);
break;
}
if(fgetc(f)==EOF)
{
break;
}
fseek(f,-1,SEEK_CUR);
memset(config_linebuf,0,sizeof(config_linebuf));
}
fclose(f);
}
/********************************************************************
* 函数名: ReadConfig(char *conf_path,char *conf_name,char *config_buff)
* 参数名: char *conf_path
* char *conf_name
char *config_buff
* 返回值: no
* 功 能: 添加修改文件(当配置文件中存在标记字段,则进行修改,若不存在则进行添加)
输入参数:1,配置文件路径 2,匹配标记 3,替换或添加的内容
* 版本 : water-2010-07-08--first
********************************************************************/
int AddOrAltConfig(char *conf_path,char *conf_name,char *config_buff)
{
char config_linebuf[256];
char line_name[40];
char *config_sign = "=";
char *leave_line;
int alter_sign = 0;
FILE *f;
f = fopen(conf_path,"r+");
if(f == NULL)
{
printf("OPEN CONFIG FALID/n");
return 0;
}
fseek(f,0,SEEK_END);
long congig_lenth = ftell(f);
int configbuf_lenth = strlen(config_buff);
configbuf_lenth = configbuf_lenth + 5;
char sum_buf[congig_lenth+configbuf_lenth];
memset(sum_buf,0,sizeof(sum_buf));
fseek(f,0,SEEK_SET);
while(fgets(config_linebuf,256,f) != NULL)
{
if(strlen(config_linebuf) < 3) //判断是否是空行
{
strcat(sum_buf,config_linebuf);
continue;
}
leave_line = NULL;
leave_line = strstr(config_linebuf,config_sign);
if(leave_line == NULL) //去除无"="的情况
{
strcat(sum_buf,config_linebuf);
continue;
}
int leave_num = leave_line - config_linebuf;
memset(line_name,0,sizeof(line_name));
strncpy(line_name,config_linebuf,leave_num);
if(strcmp(line_name,conf_name) ==0)
{
strcat(sum_buf,config_buff);
strcat(sum_buf,"/n");
alter_sign = 1;
}
else
{
strcat(sum_buf,config_linebuf);
}
if(fgetc(f)==EOF)
{
break;
}
fseek(f,-1,SEEK_CUR);
memset(config_linebuf,0,sizeof(config_linebuf));
}
if(alter_sign == 0)
{
strcat(sum_buf,config_buff);
strcat(sum_buf,"/n");
}
printf("---sum_buf---->%s<----------/n",sum_buf);
remove(conf_path);
fclose(f);
FILE *fp;
fp = fopen(conf_path,"w+");
if(fp == NULL)
{
printf("OPEN CONFIG FALID/n");
return 2;
}
fseek(fp,0,SEEK_SET);
fputs(sum_buf,fp);
fclose(fp);
}
/********************************************************************
* 函数名: ReadConfig(char *conf_path,char *conf_name,char *config_buff)
* 参数名: char *conf_path
* char *conf_name
char *config_buff
* 返回值: no
* 功 能: 删除配置文件内容输入参数:1,配置文件路径 2,匹配标记
* 版本 : water-2010-07-08--first
********************************************************************/
int DeleteConfig(char *conf_path,char *conf_name)
{
char config_linebuf[256];
char line_name[40];
char *config_sign = "=";
char *leave_line;
FILE *f;
f = fopen(conf_path,"r+");
if(f == NULL)
{
printf("OPEN CONFIG FALID/n");
return 0;
}
fseek(f,0,SEEK_END);
long congig_lenth = ftell(f);
char sum_buf[congig_lenth+2];
memset(sum_buf,0,sizeof(sum_buf));
fseek(f,0,SEEK_SET);
while(fgets(config_linebuf,256,f) != NULL)
{
if(strlen(config_linebuf) < 3) //判断是否是空行
{
strcat(sum_buf,config_linebuf);
continue;
}
leave_line = NULL;
leave_line = strstr(config_linebuf,config_sign);
if(leave_line == NULL) //去除无"="的情况
{
strcat(sum_buf,config_linebuf);
continue;
}
int leave_num = leave_line - config_linebuf;
memset(line_name,0,sizeof(line_name));
strncpy(line_name,config_linebuf,leave_num);
if(strcmp(line_name,conf_name) ==0)
{
}
else
{
strcat(sum_buf,config_linebuf);
}
if(fgetc(f)==EOF)
{
break;
}
fseek(f,-1,SEEK_CUR);
memset(config_linebuf,0,sizeof(config_linebuf));
}
printf("---sum_buf---->%s<----------/n",sum_buf);
remove(conf_path);
fclose(f);
FILE *fp;
fp = fopen(conf_path,"w+");
if(fp == NULL)
{
printf("OPEN CONFIG FALID/n");
return 2;
}
fseek(fp,0,SEEK_SET);
fputs(sum_buf,fp);
fclose(fp);
}
其实这样干实在是走了弯路。我们可以自己写个开机运行的与网络相关参数有关的脚本;
如:自己写个net_cfg.sh 如下:
#!/bin/shell
/sbin/ifconfig eth0 192.168.14.207 netmask 255.255.0.0 up
/sbin/route del default
/sbin/route add default gw 192.168.14.254
若想修改相关信息,则直接把该脚本删除了,再创建一个新的,其中写入新的配置信息即可。当然为了想要修改的信息及时起效,可以同时调用system(ifconfig......),使其ip,gateway,netmask等及时修改,这样在重启后,系统读取新的脚本,也达到了修改的目的。
6关于UDP广播
Udp广播的相关代码如下
int i = 1;// broadcast enabled
setsockopt(socketd, SOL_SOCKET, SO_BROADCAST, &i, sizeof(i));
inet_aton("192.168.255.255" , (struct in_addr *)&cliaddr.sin_addr) ;//设置为广播
192.168网段内的所有主机都会收到广播的内容。
当然要确保运行该广播程序的主机的ip的子网掩码与该程序的广播网段相对应才行。即该主机的子网掩码必须是255.255.0.0 本人曾经试过把netmask改为255.255.255.0发现广播失败。
当然最好是把广播地址设置为:
inet_aton("255.255.255.255" , (struct in_addr *)&cliaddr.sin_addr) ;//设置为广播
7几个API函数应该注意的地方
1)一般都用getaddrinfo代替较老的gethostbyname().本人用后者获取ip时,得到的每次都是127.0.0.1,因为该函数是根据etc/host中的信息得到结果的。(好像是这样)
2)指定IP为INADDR_ANY,意思是系统所安装的所有网卡都可以。
3)如果套接字已经和对方建立了连接,可以用getpeername来得到对方的地址。
4)对于建立连接的connect(),可能瞬时没有链接上导致失败,但可以用指数补偿法来使重试,是系统更稳定。代码如下:
/********************************************************************
* 函数名: connect_retry
* 功 能: connect for socket
* 版本 : come from APUE
********************************************************************/
#define MAXSLEEP 128
int
connect_retry(int sockfd, const struct sockaddr *addr, socklen_t alen)
{
int nsec;
/*
* Try to connect with exponential backoff.
*/
for (nsec = 1; nsec <= MAXSLEEP; nsec <<= 1) {
if (connect(sockfd, addr, alen) == 0) {
/*
* Connection accepted.
*/
return(0);
}
/*
* Delay before trying again.
*/
if (nsec <= MAXSLEEP/2)
usleep(nsec);
}
return(-1);
}
5)关于accept()函数:
int accept(int sockfd,struct sockaddr *addr, socklen_ *addrlen)
其返回的值是新建立链接的套接字描述符,可以通过该描述符号用getpeername获取对方的地址信息,也可以直接用形参地址的返回 sockaddr *addr,来获取地址端口信息。
6)recv函数的返回值,如果是-1,则表明对方已经断开了链接。常用此来判断对方是否断开连接。
7)在编程时注意一些函数关于sockaddrlen的参数的传递,是传递的指针还是值,别弄错。
如sendto()的最后一个参数是 int tolen(值),而recvform()的该参数是int *formlen(指针)
8)关于非阻塞connect的使用:
非阻塞模式有3种用途
1.三次握手同时做其他的处理。connect要花一个往返时间完成,从几毫秒的局域网到几百毫秒或几秒的广域网。这段时间可能有一些其他的处理要执行,比如数据准备,预处理等。
2.用这种技术建立多个连接。这在web浏览器中很普遍.
3.由于程序用select等待连接完成,可以设置一个select等待时间限制,从而缩短connect超时时间。多数实现中,connect的超时时间在75秒到几分钟之间。有时程序希望在等待一定时间内结束,使用非阻塞connect可以防止阻塞75秒,在多线程网络编程中,尤其必要。 例如有一个通过建立线程与其他主机进行socket通信的应用程序,如果建立的线程使用阻塞connect与远程通信,当有几百个线程并发的时候,由于网络延迟而全部阻塞,阻塞的线程不会释放系统的资源,同一时刻阻塞线程超过一定数量时候,系统就不再允许建立新的线程(每个进程由于进程空间的原因能产生的线程有限),如果使用非阻塞的connect,连接失败使用select等待很短时间,如果还没有连接后,线程立刻结束释放资源,防止大量线程阻塞而使程序崩溃。
目前connect非阻塞编程的普遍思路是:
在一个TCP套接口设置为非阻塞后,调用connect,connect会在系统提供的errno变量中返回一个EINRPOCESS错误,此时TCP的三路握手继续进行。之后可以用select函数检查这个连接是否建立成功。以下实验基于unix网络编程和网络上给出的普遍示例,在经过大量测试之后,发现其中有很多方法,在linux中,并不适用。
我先给出了重要源码的逐步分析,在最后给出完整的connect非阻塞源码。
1.首先填写套接字结构,包括远程的ip,通信端口如下: */
struct sockaddr_in serv_addr;
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(9999);
serv_addr.sin_addr.s_addr = inet_addr("58.31.231.255"); //inet_addr转换为网络字节序
bzero(&(serv_addr.sin_zero),8);
// 2.建立socket套接字:
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket creat error");
return 1;
}
// 3.将socket建立为非阻塞,此时socket被设置为非阻塞模式
flags = fcntl(sockfd,F_GETFL,0);//获取建立的sockfd的当前状态(非阻塞)
fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);//将当前sockfd设置为非阻塞
/*4. 建立connect连接,此时socket设置为非阻塞,connect调用后,无论连接是否建立立即返回-1,同时将errno(包含errno.h就可以直接使用)设置为EINPROGRESS, 表示此时tcp三次握手仍旧进行,如果errno不是EINPROGRESS,则说明连接错误,程序结束。
当客户端和服务器端在同一台主机上的时候,connect回马上结束,并返回0;无需等待,所以使用goto函数跳过select等待函数,直接进入连接后的处理部分。*/
if ( ( n = connect( sockfd, ( struct sockaddr *)&serv_addr , sizeof(struct sockaddr)) ) < 0 )
{
if(errno != EINPROGRESS) return 1;
}
if(n==0)
{
printf("connect completed immediately");
goto done;
}
/* 5.设置等待时间,使用select函数等待正在后台连接的connect函数,这里需要说明的是使用select监听socket描述符是否可读或者可写,如果只可写,说明连接成功,可以进行下面的操作。如果描述符既可读又可写,分为两种情况,第一种情况是socket连接出现错误(不要问为什么,这是系统规定的,可读可写时候有可能是connect连接成功后远程主机断开了连接close(socket)),第二种情况是connect连接成功,socket读缓冲区得到了远程主机发送的数据。需要通过connect连接后返回给errno的值来进行判定,或者通过调用 getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len); 函数返回值来判断是否发生错误,这里存在一个可移植性问题,在solaris中发生错误返回-1,但在其他系统中可能返回0.我首先按unix网络编程的源码进行实现。如下:*/
FD_ZERO(&rset);
FD_SET(sockfd,&rset);
wset = rset;
tval.tv_sec = 0;
tval.tv_usec = 300000;
int error;
socklen_t len;
if(( n = select(sockfd+1, &rset, &wset, NULL,&tval)) <= 0)
{
printf("time out connect error");
close(sockfd);
return -1;
}
If ( FD_ISSET(sockfd,&rset) || FD_ISSET(sockfd,&west) )
{
len = sizeof(error);
if( getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len) <0)
return 1;
}
/* 这里我测试了一下,按照unix网络编程的描述,当网络发生错误的时候,getsockopt返回-1,return -1,程序结束。网络正常时候返回0,程序继续执行。
可是我在linux下,无论网络是否发生错误,getsockopt始终返回0,不返回-1,说明linux与unix网络编程还是有些细微的差别。就是说当socket描述符可读可写的时候,这段代码不起作用。不能检测出网络是否出现故障。
我测试的方法是,当调用connect后,sleep(2)休眠2秒,借助这两秒时间将网络助手断开连接,这时候select返回2,说明套接口可读又可写,应该是网络连接的出错情况。
此时,getsockopt返回0,不起作用。获取errno的值,指示为EINPROGRESS,没有返回unix网络编程中说的ENOTCONN,EINPROGRESS表示正在试图连接,不能表示网络已经连接失败。
针对这种情况,unix网络编程中提出了另外3种方法,这3种方法,也是网络上给出的常用的非阻塞connect示例:
a.再调用connect一次。失败返回errno是EISCONN说明连接成功,表示刚才的connect成功,否则返回失败。 代码如下:*/
int connect_ok;
connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr) );
switch (errno)
{
case EISCONN: //connect ok
printf("connect OK /n");
connect_ok = 1;
break;
case EALREADY:
connect_0k = -1
break;
case EINPROGRESS: // is connecting, need to check again
connect_ok = -1
break;
default:
printf("connect fail err=%d /n",errno);
connect_ok = -1;
break;
}
/*如程序所示,根据再次调用的errno返回值将connect_ok的值,来进行下面的处理,connect_ok为1继续执行其他操作,否则程序结束。
但这种方法我在linux下测试了,当发生错误的时候,socket描述符(我的程序里是sockfd)变成可读且可写,但第二次调用connect 后,errno并没有返回EISCONN,,也没有返回连接失败的错误,仍旧是EINPROGRESS,而当网络不发生故障的时候,第二次使用 connect连接也返回EINPROGRESS,因此也无法通过再次connect来判断连接是否成功。
b.unix网络编程中说使用read函数,如果失败,表示connect失败,返回的errno指明了失败原因,但这种方法在linux上行不通,linux在socket描述符为可读可写的时候,read返回0,并不会置errno为错误。
c.unix网络编程中说使用getpeername函数,如果连接失败,调用该函数后,通过errno来判断第一次连接是否成功,但我试过了,无论网络连接是否成功,errno都没变化,都为EINPROGRESS,无法判断。
悲哀啊,即使调用getpeername函数,getsockopt函数仍旧不行。
综上方法,既然都不能确切知道非阻塞connect是否成功,所以我直接当描述符可读可写的情况下进行发送,通过能否获取服务器的返回值来判断是否成功。(如果服务器端的设计不发送数据,那就悲哀了。)
程序的书写形式出于可移植性考虑,按照unix网络编程推荐写法,使用getsocketopt进行判断,但不通过返回值来判断,而通过函数的返回参数来判断。
6. 用select查看接收描述符,如果可读,就读出数据,程序结束。在接收数据的时候注意要先对先前的rset重新赋值为描述符,因为select会对 rset清零,当调用select后,如果socket没有变为可读,则rset在select会被置零。所以如果在程序中使用了rset,最好在使用时候重新对rset赋值。
程序如下:*/
FD_ZERO(&rset);
FD_SET(sockfd,&rset);//如果前面select使用了rset,最好重新赋值
if( ( n = select(sockfd+1,&rset,NULL, NULL,&tval)) <= 0 )
{
close(sockfd);
return -1;
}
if ((recvbytes=recv(sockfd, buf, 1024, 0)) ==-1)
{
perror("recv error!");
close(sockfd);
return 1;
}
printf("receive num %d/n",recvbytes);
printf("%s/n",buf);
*/
缺省创建的线程属性经测试为:
SCHED_OTHER(非实时策略,时间片)
SCOPE_SYSTEM(系统竞争范围
PTHREAD_INHERIT_SCHED(继承主线程属性)
PTHREAD_CREATE_JOINABLE(非分离)
在多个线程访问临界资源时,最好用互斥锁,来使一次只能让一个线程访问。
也可以用信号量(无名信号量,进程中用的是有名信号量)来实现线程的同步与互斥。
条件变量也常用;
线程之间的内存共享的,各个线程之间不需要像进程之间那样用一些操作系统提供的机制来实现通信。我们可以自己建立一些数据结构,如队列,链表等,再加上一些阻塞的机制,如信号量,互斥锁等。来实现如进程进制中的通信。
经测试实验,在线程中也可以用有名管道,消息队列进行通信。
有名管道的读写在其数据小于系统定义的PIPE_BUF时,是原子操作。无需做临界访问相关的保护。
关于线程属性的详细总结,请看另外一篇《关于linux线程调度策略的学习》
关于线程条件变量的用法(线程使用手册上有很好的说明)
本人一般这样用:
//等待sample_flag ==1 为条件
While(1)
{
pthread_mutex_lock(&sample_lock);
while( !sample_flag)
{
dprintf("wait sample cond /n");
pthread_cond_wait( &sample_cond, &sample_lock);
}
pthread_mutex_unlock(&sample_lock);
Do something ............
}
// 清标志
pthread_mutex_lock(&sample_lock);
sample_flag = 0;
pthread_mutex_unlock(&sample_lock);
//
//置位标志
pthread_mutex_lock(&sample_lock);
sample_flag = 1;
pthread_cond_broadcast(&sample_cond); //广播信号量,启动等待的语音视频接收线程
pthread_mutex_unlock(&sample_lock);
文件io
对于文件的读写,优先使用标准io.一是因为标志i/o提供缓冲流,可以尽量减少系统调用的数量,同时标准i/o方便移植。一般与硬件相关的操作都用基于文件描述符的i/o操作(其兼容posix标准)。
Read(),Write(), fread(),fwrite函数后,文件位置都会自己变化。其返回值都为实际读取或写入的数,read(),fread若返回0,则表示已经达到文件尾。
可以用lseek()函数,来指定文件的偏移量。
Open()和fopen()打开文件的具体情况,选择具体的参数。一般如果尽量以只读或只写来打开(如果打开该文件只为了写,则以只写打开,不要以读写打开),这样来避免在后面的操作中的一些习惯错误操作。
注意现象:
1)如果一个文件正在使用(如已经open未close),则用标准IO操作时(fopen)可能导致打开失败,而用基于文件描述符的IO(open)则没有此现象。
2)养成好习惯,在哪个函数中打开文件,则在该文件中关闭文件。在不用时该文件了,就尽快把其关闭。以免在未关闭的情况下,系统断电,重启等。可能导致文件数据问题。
多路复用:
Slect()也常用来做定时,sleep等。APUE上说其比sleep.usleep等好用
POLL()