Socket 相关的知识

时间:2023-03-09 02:13:19
Socket 相关的知识

1、关于PF_INET和AF_INET的区别

在写网络程序的时候,建立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,))==-)
{
fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
exit();
} /* 服务器端填充 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) >)
write(s,buf,strlen(buffer));
close(s);
exit();
}
else {
while( (n=read(s,buffer,sizeof(buffer)){
do_something(n,buffer);

wait();
exit();
}

对于这段代码,我们所期望的是子进程获取完标准终端的数据,写入套接字后close套接字,并退出,服务器端接收完数据检测到EOF(表示数据已发送完),也关闭连接,并退出。接着父进程读取完服务器端响应的数据,并退出。然而,事实会是这样子的嘛,其实不然!子进程close套接字后,套接字对于父进程来说仍然是可读和可写的,尽管父进程永远都不会写入数据。因此,此socket的断连过程没有发生,因此,服务器端就不会检测到EOF标识,会一直等待从客户端来的数据。而此时父进程也不会检测到服务器端发来的EOF标识。这样服务器端和客户端陷入了死锁(deadlock)。如果用shutdown代替close,则会避免死锁的发生。

if( fork() ) {
while( gets(buffer)
write(s,buffer,strlen(buffer)); shutdown(s,);
exit();
}

 3、当客户端保持着与服务器端的连接,这时服务器端断开,再开启服务器时会出现: Address already in usr
      可以用netstat -anp | more 可以看到客户端还保持着与服务器的连接(还在使用服务器bind的端口)。这是由于client没有执行close,连接还会等待client的FIN包一段时间。解决方法是使用setsockopt,使得socket可以被重用,是最常用的服务器编程要点。具体的做法为是,在socket调用和bind调用之间加上一段对socket的设置:

int opt = ;
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[], gate[];
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 -;
} if(!fgets(gate, , 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 ;
} int get_net_info(int devno, dvr_net_info_t *inet)
{
int ret, fd;
char dev[];
struct ifreq ifr; fd = socket(AF_INET, SOCK_DGRAM, ); /* 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-); //# 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 ;
}
#endif ret = ioctl(fd, SIOCGIFADDR, &ifr);
if(ret<)
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<)
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, ); return ;
} int set_net_info(int devno, dvr_net_info_t *inet)
{
int ret, fd;
char dev[], cmd[];
struct ifreq ifr;
struct sockaddr_in sin; if(!strcmp(inet->ip, "0.0.0.0"))
return -; sprintf(dev, "eth%d", devno); fd = socket(AF_INET, SOCK_DGRAM, );
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();
} 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 < )
dprintf("netmask: Invalid argument\n"); gateway_info(dev, inet->gate, );
} 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当中取得就可以加以记录了

 6、gethostname和gethostbyname

  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[]); /* 调用gethostbyname()。调用结果都存在hp中 注意有的获取IP地址的时候,需要去掉“://”前面的和“/”后面的*/
if(hp==NULL)
{
(void)printf("host information for %s not found\n",argv[]);
exit();
}
for(p=hp->h_addr_list;*p!=;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!=;q++)
printf("%s",*q);
putchar('\n');
}
exit();
}

 11.udp数据发送

  udp数据发送的目标地址,一个固定值,一个接受到的地址,如果经过了路由器,那么这个接受地址变成了路由器地址,再次发送就不是已经的目标地址,而tcp的面向对象连接,建立了链路,所以远程的时候或者访问上级子网的时候,要么采取tcp的,要么采取udp 固定ip地址发送。