Socket 相关的知识
在写网络程序的时候,建立TCP socket:
sock = socket(PF_INET, SOCK_STREAM,
0);
然后在绑定本地地址或连接远程地址时需要初始化sockaddr_in结构,其中指定address
family时一般设置为AF_INET,即使用IP。
相关头文件中的定义:AF = Address Family
PF = Protocol Family
AF_INET
= PF_INET
在windows中的Winsock2.h中,
#define AF_INET 0
#define PF_INET AF_INET
所以在windows中AF_INET与PF_INET完全一样.
而在Unix/Linux系统中,在不同的版本中这两者有微小差别.对于BSD,是AF,对于POSIX是PF.
理论上建立socket时是指定协议,应该用PF_xxxx,设置地址时应该用AF_xxxx。当然AF_INET和PF_INET的值是相同的,混用也不会有太大的问题。也就是说你socket时候用PF_xxxx,设置的时候用AF_xxxx也是没关系的,这点随便找个TCPIP例子就可以验证出来了。如下,不论是AF_INET还是PF_INET都是可行的,只不过这样子的话,有点不符合规范。
/* 服务器端开始建立socket描述符 */ // if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) if((sockfd=socket(PF_INET,SOCK_STREAM,0))==-1) { fprintf(stderr,"Socket error:%s\n\a",strerror(errno)); exit(1); } /* 服务器端填充 sockaddr结构 */ bzero(&server_addr,sizeof(struct sockaddr_in)); server_addr.sin_family=AF_INET; //server_addr.sin_family=PF_INET; server_addr.sin_addr.s_addr=htonl(INADDR_ANY); server_addr.sin_port=htons(portnumber);
在函数socketpair与socket的domain参数中有AF_UNIX,AF_LOCAL,AF_INET,PF_UNIX,PF_LOCAL,PF_INET.
这几个参数有AF_UNIX=AF_LOCAL, PF_UNIX=PF_LOCAL, AF_LOCAL=PF_LOCAL, AF_INET=PF_INET.
但是对于socketpair与socket的domain参数,使用PF_LOCAL系列,
而在初始化套接口地址结构时,则使用AF_LOCAL.
例如:
z = socket(PF_LOCAL, SOCK_STREAM, 0);
adr_unix.sin_family = AF_LOCAL;
Linux 下的address family定义
/* Supported address families. */ #define AF_UNSPEC 0 #define AF_UNIX 1 /* Unix domain sockets */ #define AF_LOCAL 1 /* POSIX name for AF_UNIX */ #define AF_INET 2 /* Internet IP Protocol */ #define AF_AX25 3 /* Amateur Radio AX.25 */ #define AF_IPX 4 /* Novell IPX */ #define AF_APPLETALK 5 /* AppleTalk DDP */ #define AF_NETROM 6 /* Amateur Radio NET/ROM */ #define AF_BRIDGE 7 /* Multiprotocol bridge */ #define AF_ATMPVC 8 /* ATM PVCs */ #define AF_X25 9 /* Reserved for X.25 project */ #define AF_INET6 10 /* IP version 6 */ #define AF_ROSE 11 /* Amateur Radio X.25 PLP */ #define AF_DECnet 12 /* Reserved for DECnet project */ #define AF_NETBEUI 13 /* Reserved for 802.2LLC project*/ #define AF_SECURITY 14 /* Security callback pseudo AF */ #define AF_KEY 15 /* PF_KEY key management API */ #define AF_NETLINK 16 #define AF_ROUTE AF_NETLINK /* Alias to emulate 4.4BSD */ #define AF_PACKET 17 /* Packet family */ #define AF_ASH 18 /* Ash */ #define AF_ECONET 19 /* Acorn Econet */ #define AF_ATMSVC 20 /* ATM SVCs */ #define AF_RDS 21 /* RDS sockets */ #define AF_SNA 22 /* Linux SNA Project (nutters!) */ #define AF_IRDA 23 /* IRDA sockets */ #define AF_PPPOX 24 /* PPPoX sockets */ #define AF_WANPIPE 25 /* Wanpipe API Sockets */ #define AF_LLC 26 /* Linux LLC */ #define AF_CAN 29 /* Controller Area Network */ #define AF_TIPC 30 /* TIPC sockets */ #define AF_BLUETOOTH 31 /* Bluetooth sockets */ #define AF_IUCV 32 /* IUCV sockets */ #define AF_RXRPC 33 /* RxRPC sockets */ #define AF_ISDN 34 /* mISDN sockets */ #define AF_PHONET 35 /* Phonet sockets */ #define AF_IEEE802154 36 /* IEEE802154 sockets */ #define AF_CAIF 37 /* CAIF sockets */ #define AF_MAX 38 /* For now.. */ /* Protocol families, same as address families. */ #define PF_UNSPEC AF_UNSPEC #define PF_UNIX AF_UNIX #define PF_LOCAL AF_LOCAL #define PF_INET AF_INET #define PF_AX25 AF_AX25 #define PF_IPX AF_IPX #define PF_APPLETALK AF_APPLETALK #define PF_NETROM AF_NETROM #define PF_BRIDGE AF_BRIDGE #define PF_ATMPVC AF_ATMPVC #define PF_X25 AF_X25 #define PF_INET6 AF_INET6 #define PF_ROSE AF_ROSE #define PF_DECnet AF_DECnet #define PF_NETBEUI AF_NETBEUI #define PF_SECURITY AF_SECURITY #define PF_KEY AF_KEY #define PF_NETLINK AF_NETLINK #define PF_ROUTE AF_ROUTE #define PF_PACKET AF_PACKET #define PF_ASH AF_ASH #define PF_ECONET AF_ECONET #define PF_ATMSVC AF_ATMSVC #define PF_RDS AF_RDS #define PF_SNA AF_SNA #define PF_IRDA AF_IRDA #define PF_PPPOX AF_PPPOX #define PF_WANPIPE AF_WANPIPE #define PF_LLC AF_LLC #define PF_CAN AF_CAN #define PF_TIPC AF_TIPC #define PF_BLUETOOTH AF_BLUETOOTH #define PF_IUCV AF_IUCV #define PF_RXRPC AF_RXRPC #define PF_ISDN AF_ISDN #define PF_PHONET AF_PHONET #define PF_IEEE802154 AF_IEEE802154 #define PF_CAIF AF_CAIF #define PF_MAX AF_MAX
2、linux之shutdown()与close()函数
1.close()函数
int close(int sockfd); //返回成功为0,出错为-1.
close 一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数,然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。
在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共享着的进程个数,当父进程或某一子进程close掉套接字时,描述符引用计数会相应的减一,当引用计数仍大于零时,这个close调用就不会引发TCP的四路握手断连过程。
2.shutdown()函数
int shutdown(int sockfd,int howto); //返回成功为0,出错为-1.
该函数的行为依赖于howto的值
1.SHUT_RD:值为0,关闭连接的读这一半。
2.SHUT_WR:值为1,关闭连接的写这一半。
3.SHUT_RDWR:值为2,连接的读和写都关闭。
终止网络连接的通用方法是调用close函数。但使用shutdown能更好的控制断连过程(使用第二个参数)。
3.两函数的区别
close与shutdown的区别主要表现在:
close函数会关闭套接字ID,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,并且有时候这是非常重要的,特别是对于多进程并发服务器来说。
而shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零,那些试图读得进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号,同时可利用shutdown的第二个参数选择断连的方式。
下面将展示一个客户端例子片段来说明使用close和shutdown所带来的不同结果:
客户端有两个进程,父进程和子进程,子进程是在父进程和服务器建连之后fork出来的,子进程发送标准输入终端键盘输入数据到服务器端,知道接收到EOF标识,父进程则接受来自服务器端的响应数据。
s=connect(...); if( fork() ){ while( gets(buffer) >0) write(s,buf,strlen(buffer));
close(s); exit(0); } else { while( (n=read(s,buffer,sizeof(buffer)){ do_something(n,buffer);
wait(0); exit(0); }
对于这段代码,我们所期望的是子进程获取完标准终端的数据,写入套接字后close套接字,并退出,服务器端接收完数据检测到EOF(表示数据已发送完),也关闭连接,并退出。接着父进程读取完服务器端响应的数据,并退出。然而,事实会是这样子的嘛,其实不然!子进程close套接字后,套接字对于父进程来说仍然是可读和可写的,尽管父进程永远都不会写入数据。因此,此socket的断连过程没有发生,因此,服务器端就不会检测到EOF标识,会一直等待从客户端来的数据。而此时父进程也不会检测到服务器端发来的EOF标识。这样服务器端和客户端陷入了死锁(deadlock)。如果用shutdown代替close,则会避免死锁的发生。
if( fork() ) { while( gets(buffer) write(s,buffer,strlen(buffer)); shutdown(s,1); exit(0); }
3、当客户端保持着与服务器端的连接,这时服务器端断开,再开启服务器时会出现: Address already in usr
可以用netstat -anp | more 可以看到客户端还保持着与服务器的连接(还在使用服务器bind的端口)。这是由于client没有执行close,连接还会等待client的FIN包一段时间。解决方法是使用setsockopt,使得socket可以被重用,是最常用的服务器编程要点。具体的做法为是,在socket调用和bind调用之间加上一段对socket的设置:
int opt = 1;
setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
4、 指定网卡IP信息设置
/*---------------------------------------------------------------------------- Network Info -----------------------------------------------------------------------------*/ static int gateway_info(char *dev, char *gateway, int set) { FILE *fp; unsigned char buf[128], gate[16]; unsigned char *find; //# get gateway sprintf(buf, "route -n | grep 'UG[ \t]' | grep %s | awk '{print $2}'", dev); fp = popen(buf, "r"); if(NULL == fp) { eprintf("popen error (%s)\n", buf); return -1; } if(!fgets(gate, 16, fp)) { strcpy(gate, "0.0.0.0"); } else { find = strchr(gate,'\n'); //# remove '\n' if(find) *find='\0'; } pclose(fp); if(set) //# set gateway { if(strcmp(gate, "0.0.0.0")) { sprintf(buf, "route del default gw %s %s", gate, dev); system_user(buf); } sprintf(buf, "route add default gw %s %s", gateway, dev); system_user(buf); } else { strcpy(gateway, gate); } return 0; } int get_net_info(int devno, dvr_net_info_t *inet) { int ret, fd; char dev[8]; struct ifreq ifr; fd = socket(AF_INET, SOCK_DGRAM, 0); /* I want to get an IPv4 IP address */ ifr.ifr_addr.sa_family = AF_INET; /* I want IP address attached to "eth0" */ sprintf(dev, "eth%d", devno); strncpy(ifr.ifr_name, dev, IFNAMSIZ-1); //# check up/down ioctl(fd, SIOCGIFFLAGS, &ifr); inet->state = ifr.ifr_flags & IFF_UP; #if 1 if(!inet->state) { //# down close(fd); strcpy(inet->ip, "0.0.0.0"); strcpy(inet->mask, "255.255.255.0"); strcpy(inet->gate, "0.0.0.0"); return 0; } #endif ret = ioctl(fd, SIOCGIFADDR, &ifr); if(ret<0) strcpy(inet->ip, "0.0.0.0"); else sprintf(inet->ip, "%s", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr)); ret = ioctl(fd, SIOCGIFNETMASK, &ifr); if(ret<0) strcpy(inet->mask, "255.255.255.0"); else sprintf(inet->mask, "%s", inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr)); close(fd); gateway_info(dev, inet->gate, 0); return 0; } int set_net_info(int devno, dvr_net_info_t *inet) { int ret, fd; char dev[8], cmd[32]; struct ifreq ifr; struct sockaddr_in sin; if(!strcmp(inet->ip, "0.0.0.0")) return -1; sprintf(dev, "eth%d", devno); fd = socket(AF_INET, SOCK_DGRAM, 0); strncpy(ifr.ifr_name, dev, IFNAMSIZ); //# check up/down ioctl(fd, SIOCGIFFLAGS, &ifr); inet->state = ifr.ifr_flags & IFF_UP; if(!inet->state) { //# down ifr.ifr_flags |= IFF_UP; ioctl(fd, SIOCSIFFLAGS, &ifr); sleep(1); } if(inet->type == NET_DHCP) { sprintf(cmd, "udhcpc -n -i %s", dev); ret = system_user(cmd); } else if(inet->type == NET_STATIC) { //memset(&sin, 0, sizeof(struct sockaddr)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = inet_addr(inet->ip); memcpy(&ifr.ifr_addr, &sin, sizeof(struct sockaddr)); ioctl(fd, SIOCSIFADDR, &ifr); sin.sin_addr.s_addr = inet_addr(inet->mask); memcpy(&ifr.ifr_addr, &sin, sizeof(struct sockaddr)); ret = ioctl(fd, SIOCSIFNETMASK, &ifr); if(ret < 0) dprintf("netmask: Invalid argument\n"); gateway_info(dev, inet->gate, 1); } close(fd); return ret; }
5、recv和recvform
recv和recvfrom都可用TCP或者UDP,只是习惯性TCP用recv,因为基于连接的对方socket是已知的,UDP用recvfrom,因为一般用没有bind远端socket,接受到本地端口的所有数据,需要recvfrom识别远端地址。
recv的recvfrom是可以替换使用的,只是recvfrom多了两个参数,可以用来接收对端的地址信息,这个对于udp这种无连接的,可以很方便地进行回复。而换过来如果你在udp当中也使用recv,那么就不知道该回复给谁了,如果你不需要回复的话,也是可以使用的。另外就是对于tcp是已经知道对端的,就没必要每次接收还多收一个地址,没有意义,要取地址信息,在accept当中取得就可以加以记录了
gethostname() -- 获取进程所在机器的计算机的名字。
gethostbyname() -- 用域名或主机名获取IP地址,这个域名或主机名可以是本地机器的主机名/域名;也可以是远端节点的域名。
7、Linux下端口复用(SO_REUSEADDR与SO_REUSEPORT)
只考虑AF_INET的情况(同一端口指ip地址与端口号都相同)
1.freebsd支持SO_REUSEPORT和SO_REUSEADDR选项,而linux只支持SO_REUSEADDR选项。
2.freebsd下,使用SO_REUSEPORT选项,两个tcp的socket可以绑定同一个端口;同样,使用SO_REUSEPORT选项,两个udp的socket可以绑定同一个端口。
3.linux下,两个tcp的socket不能绑定同一个端口;而如果使用SO_REUSEADDR选项,两个udp的socket可以绑定同一个端口。
4.freebsd下,两个tcp的socket绑定同一端口,只有第一个socket获得数据。
5.freebsd下,两个udp的socket绑定同一端口,如果数据包的目的地址是单播地址,则只有第一个socket获得数据,而如果数据包的目的地址是多播地址,则两个socket同时获得相同的数据。
6.linux下,两个udp的socket绑定同一端口,如果数据包的目的地址是单播地址,则只有最后一个socket获得数据,而如果数据包的目的地址是多播地址,则两个socket同时获得相同的数据。
SO_REUSEADDR提供如下四个功能:
1.SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。
2.SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。对于TCP,我们根本不可能启动捆绑相同IP地址和相同端口号的多个服务器。
3.SO_REUSEADDR允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。
4.SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。
SO_REUSEPORT选项有如下语义:
此选项允许完全重复捆绑,但仅在想捆绑相同IP地址和端口的套接口都指定了此套接口选项才性。
如果被捆绑的IP地址是一个多播地址,则SO_REUSEADDR和SO_REUSEPORT等效。
使用这两个套接口选项的建议:
在所有TCP服务器中,在调用bind之前设置SO_REUSEADDR套接口选项;
当编写一个同一时刻在同一主机上可运行多次的多播应用程序时,设置SO_REUSEADDR选项,并将本组的多播地址作为本地IP地址捆绑。
8、UDP 调用 connect的作用
1.UDP中可以使用connect系统调用
2.UDP中connect操作与TCP中connect操作有着本质区别。
TCP中调用connect会引起三次握手,client与server建立连结.UDP中调用connect内核仅仅把对端ip&port记录下来。
3.UDP中可以多次调用connect,TCP只能调用一次connect。
UDP多次调用connect有两种用途:
1,指定一个新的ip&port连结,指定新连结,直接设置connect第二个参数即可。
2,断开和之前的ip&port的连结,断开连结,需要将connect第二个参数中的sin_family设置成 AF_UNSPEC即可。
4.UDP中使用connect可以提高效率.原因如下:
普通的UDP发送两个报文内核做了如下:
#1:建立连结
#2:发送报文
#3:断开连结
#4:建立连结
#5:发送报文
#6:断开连结
采用connect方式的UDP发送两个报文内核如下处理:
#1:建立连结
#2:发送报文
#3:发送报文另外一点, 每次发送报文内核都由可能要做路由查询。
5.采用connect的UDP发送接受报文可以调用send,write和recv,read操作.当然也可以调用sendto,recvfrom.
调用sendto的时候第五个参数必须是NULL,第六个参数是0,调用recvfrom,recv,read系统调用只能获取到先前connect的ip&port发送的报文。
6.UDP中使用connect的好处:
1:会提升效率.前面已经描述了.
2:高并发服务中会增加系统稳定性.原因:
假设client A 通过非connect的UDP与server B,C通信.B,C提供相同服务。为了负载均衡,我们让A与B,C交替通信。A 与 B通信IPa:PORTa <----> IPb:PORTb;A与 C通信IPa:PORTa' <---->IPc:PORTc 。假设PORTa与PORTa'相同了(在大并发情况下会发生这种情况),那么就有可能出现A等待B的报文,却收到了C的报文.导致收报错误.解决方法内就是采用connect的UDP通信方式.在A中创建两个udp,然后分别connect到B,C。
9、getsockname与getpeername
getsockname()是返回套接口关联的本地协议地址。
getpeername()是返回套接口关联的远程协议地址。
getsockname和getpeername调度时机很重要,如果调用时机不对,则无法正确获得地址和端口。
TCP:
1>对于服务器来说,在bind以后就可以调用getsockname来获取本地地址和端口,虽然这没有什么太多的意义。getpeername只有在链接建立以后才调用,否则不能正确获得对方地址和端口,所以他的参数描述字一般是链接描述字而非监听套接口描述字。
2>对于客户端来说,在调用socket时候内核还不会分配IP和端口,此时调用getsockname不会获得正确的端口和地址(当然链接没建立更不可能调用getpeername),当然如果调用了bind 以后可以使用getsockname。想要正确的到对方地址(一般客户端不需要这个功能),则必须在链接建立以后,同样链接建立以后,此时客户端地址和端口就已经被指定,此时是调用getsockname的时机。
UDP:
UDP分为链接和没有链接2种:
1>没有链接的UDP不能调用getpeername,但是可以调用getsockname,和TCP一样,他的地址和端口不是在调用socket就指定了,而是在第一次调用sendto函数以后
2>已经链接的UDP,在调用connect以后,这2个函数都是可以用的(同样,getpeername也没太大意义。如果你不知道对方的地址和端口,不可能会调用connect)。
10、用域名取得主机的ip地址(gethostbyname)
使用gethostbyname()函数包含2个头文件:
#include <netdb.h>
#include <sys/socket.h>
gethostbyname()函数定义:
struct hostent *gethostbyname(const char *name);
这个函数的传入值是域名或者主机名,例如"www.google.com","wpc"等等;传出值,是一个hostent的结构(如下)。如果函数调用失败,将返回NULL
hostent结构体定义:
struct hostent { char *h_name; //表示的是主机的规范名。例如www.google.com的规范名其实是www.l.google.com。 char **h_aliases; // 表示的是主机的别名。www.google.com就是google他自己的别名。有的时候,有的主机可能有好几个别名,这些,其实都是为了易于用户记忆而为自己的网站多取的名字。 int h_addrtype; // 表示的是主机ip地址的类型,到底是ipv4(AF_INET),还是ipv6(AF_INET6) int h_length; //表示的是主机ip地址的长度 char **h_addr_list; //表示的是主机的ip地址,注意,这个是以网络字节序存储的。千万不要直接用printf带%s参数来打这个东西,会有问题的哇。所以到真正需要打印出这个IP的话,需要调用inet_ntop()。 };
inet_ntop()函数定义:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt) :
这个函数,是将类型为af的网络地址结构src,转换成主机序的字符串形式,存放在长度为cnt的字符串中。
这个函数,其实就是返回指向dst的一个指针。如果函数调用错误,返回值是NULL。
下面是例程,就是通过给定一个主机名,然后调用gethostbyname(hostname),返回一个struct hostent类型的数据结构其中包含 char **h_addr_list(为ip地址列表),然后调用inet_ntoa(struct in_addr in)打印出ip地址。
#include<stdio.h> #include<string.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<netdb.h> main(int argc,const char **argv) { long addr; struct hostent *hp; char **p; hp=gethostbyname(argv[1]); /* 调用gethostbyname()。调用结果都存在hp中 注意有的获取IP地址的时候,需要去掉“://”前面的和“/”后面的*/ if(hp==NULL) { (void)printf("host information for %s not found\n",argv[1]); exit(2); }
for(p=hp->h_addr_list;*p!=0;p++) { struct in_addr in; char **q;
memcpy(&in.s_addr,*p,sizeof(in.s_addr));
printf("%s\t%s",inet_ntoa(in),hp->h_name);/* 将刚才得到的所有地址都打出来。其中调用了inet_ntoa()函数 */ for(q=hp->h_aliases;*q!=0;q++) printf("%s",*q); putchar('\n'); } exit(0); }
11.udp数据发送
udp数据发送的目标地址,一个固定值,一个接受到的地址,如果经过了路由器,那么这个接受地址变成了路由器地址,再次发送就不是已经的目标地址,而tcp的面向对象连接,建立了链路,所以远程的时候或者访问上级子网的时候,要么采取tcp的,要么采取udp 固定ip地址发送。