套接字是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行,Linux所提供的功能(如打印服 务,ftp等)通常都是通过套接字来进行通信的,套接字的创建和使用与管道是有区别的,因为套接字明确地将客户和服务器区分出来,套接字可以实现将多个客 户连接到一个服务器。
套接字属性
套接字的特性由3个属性确定,他们是,域,类型和协议
域指定套接字通信中使用的网络介质,最常见的套接字域是AF_INET,它指的是Internet网络
套接字类型
一个套接字可能有多种不同的通信方式
流套接字,流套接字提供一个有序,可靠,双向节流的链接,流套接字由类型SOCK_STREAM指定,它是在AF_INET域中通过TCP/IP链接实现的,这就是套接字类型(其实就是通信方式)
与流套接字相反,由类型SOCK_DGRAM指定的数据报套接字不建立和维持一个连接,它对可以发送的数据长度有限制,数据报作为一个单独的网络消息被传输,它可能会丢失,复制或乱序
最后一个是套接字协议,通常使用默认就可以了(也就是最后一个参数填0)
创建套接字
socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字
#include
#include
int socket(int domain,int type,int protocol);
创建的套接字是一条通信线路的一个端点,domain参数指定协议族(使用的网络介质),type参数指定这个套接字的通信类型(通信方式),protocot参数指定使用的协议
domain参数可以指定如下协议族
AF_UNIX UNIX域协议(文件系统套接字)
AF_INET ARPA因特网协议
AF_ISSO ISO标准协议
AF_NS Xerox网络协议
AF_IPX Novell IPX协议
AF_APPLETALK Appletalk DDS协议
最常用的套接字域是AF_UNIX和AF_INET,前者用于通过UNIX和Linux文件系统实现本地套接字
socket函数的第二个参数type指定用于新套接字的特性,它的取值包括SOCK_STREAM和SOCK_DGRAM
SOCK_STREAM是一个有序,可靠,面向连接的双向字节流,一般用这个
最后一个protocol参数,将参数设为0表示使用默认协议。
套接字地址
每个套接字(端点)都有其自己的地址格式,对于AF_UNIX套接字来说,它的地址由结构sockaddr_un来描述,该结构体定义在头文件sys/un.h中,如下:
struct sockaddr_un {
sa_family_t sun_family; //套接字域
char sun_path[];//名字
};
而在AF_INET域中,套接字地址结构由sockaddr_in来指定,该结构体定义在头文件netinet/in.h中
struct sockaddr_in {
short int sin_family;//套接字域
unsigned short int sin_port;//接口
struct in_addr sin_addr;
}
IP地址结构in_addr被定义如下:
struct in_addr {
unsigned long int s_addr;
};
命名套接字
要想让通过socket调用创建的套接字可以被其它进程使用,服务器程序就必须给该套接字命名,如下,AF_INET套接字就会关联到一个IP端口号
#include
int bind(int socket,const struct sockaddr *address,size_t address_len);
bind系统调用把参数address中的地址分配给与文件描述符socket关联的未命名套接字
创建套接字队列
为了能够在套接字上接受进入链接,服务器程序必须创建一个队列来保存未处理的请求,它用listen系统调用来完成这一工作
#include
int listen(int socket,int backlog);
Linux系统可能会对队列中未处理的连接的最大数目做出限制
接受连接
一旦服务器程序创建并命名套接字之后,他就可以通过accept系统调用来等待客户建立对该套接字的连接
#include
int accept(int socket,struct sockaddr *address,size_t *address_len);
accept函数将创建一个新套接字来与该客户进行通信,并且返回新套接字描述符(这个描述符和客户端中描述符是一样等同)
请求连接
客户程序通过一个未命名套接字和服务器监听套接字之间建立的连接的方法来连接到服务器,如下:
#include
int connect(int socket,const struct sockaddr *address,size_t address_len);
参数socket指定的套接字将连接到参数address指定的服务器套接字
关闭套接字
你可以通过调用close函数来终止服务器和客户上的套接字连接
套接字通信
套接字可以通过调用read(),write()系统调用来进行传输数据
下面是套接字的一些例子
一个简单的本地客户
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd;
int len;
struct sockaddr_un
address;//套接字地址
int result;
char ch = 'A';
sockfd = socket(AF_UNIX,SOCK_STREAM,0);//创建一个套接字(端点),并返回一个描述符
address.sun_family =
AF_UNIX;//指明网络介质
strcpy(address.sun_path,"server_socket");// 名字
len = sizeof(address);
result = connect(sockfd,(struct sockaddr *)&address,len);//请求连接到address
if(result == -1) {
perror("oops:
clientl");
exit(1);
}
write(sockfd,&ch,1);//把数据写入套接字
read(sockfd,&ch,1);//服务器处理后读出处理后数据
printf("char form server =
%c
",ch);
close(sockfd);
exit(0);
}
下面是一个本地服务器
#include
#include
#include
#include
#include
#include
int main()
{
int
server_sockfd,client_sockfd;//定义套接字描述符
int
server_len,client_len;
struct sockaddr_un
server_address;//套接字地址
struct sockaddr_un
client_address;//套接字地址
unlink("server_socket");
server_sockfd =
socket(AF_UNIX,SOCK_STREAM,0);//创建一个套接字,并返回一个描述符
server_address.sun_family =
AF_UNIX;//指定网络介质
strcpy(server_address.sun_path,"server_socket");//名字
server_len =
sizeof(server_address);
bind(server_sockfd,(struct
sockaddr *)&server_address,server_len);//命名套接字
listen(server_sockfd,5);//创建套接字队列
while(1) {
char
ch;
printf("server
waiting
");
client_len
= sizeof(client_address);
client_sockfd
= accept(server_sockfd,(struct sockaddr
*)&client_address,&client_len);//接受连接
read(client_sockfd,&ch,1);//从套接字中读取数据
ch++;// 处理数据
write(client_sockfd,&ch,1);//把数据重新写回套接字
close(client_sockfd);
}
}
这两个程序运行如下:
root@www:/opt/chengxu# ./server1
&
[3] 4644
root@www:/opt/chengxu#
server waiting
root@www:/opt/chengxu# ./client1
& ./client1 & ./client1
&
[4] 4652
[5] 4653
[6] 4654
root@www:/opt/chengxu#
server waiting
server waiting
server waiting
char form server = B
char form server = B
char form server = B
[4]
Done
./client1
[5]-
Done
./client1
[6]+
Done
下面看一个网络套接字的例子,先看server程序:
#include
#include
#include
#include
#include
#include
#include
int main()
{
int
server_sockfd,client_sockfd;//定义套接字描述符
int
server_len,client_len;
struct sockaddr_in
server_address;//套接字地址结构体
struct sockaddr_in
client_address;//套接字地址结构体
unlink("server_socket");
server_sockfd =
socket(AF_INET,SOCK_STREAM,0);//创建一个套接字,并返回一个描述符
server_address.sin_family =
AF_INET;//指定网络介质
//server_address.sin_addr.s_addr
= inet_addr("127.0.0.1");
server_address.sin_addr.s_addr
= htonl(INADDR_ANY);//客户连接到服务器的IP
server_address.sin_port =
htons(9734);//客户连接到端口
server_len =
sizeof(server_address);
bind(server_sockfd,(struct
sockaddr *)&server_address,server_len);//命名套接字
listen(server_sockfd,5);//创建套接字队列
while(1) {
char
ch;
printf("server
waiting
");
client_len
= sizeof(client_address);
client_sockfd
= accept(server_sockfd,(struct sockaddr
*)&client_address,&client_len);//接受连接
read(client_sockfd,&ch,1);//从套接字中读取数据
ch++;//处理数据
write(client_sockfd,&ch,1);//把处理后数据写入套接字
close(client_sockfd);//关闭套接字
}
}
下面是客户端程序:
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd;//套接字描述符
int len;
struct sockaddr_in
address;//套接字地址结构体
int result;
char ch = 'A';
sockfd = socket(AF_INET,SOCK_STREAM,0);//创建一个套接字并返回一个描述符
address.sin_family =
AF_INET;// 指定网络介质
address.sin_addr.s_addr =
htonl(INADDR_ANY);//要连接到主机IP
address.sin_port =
htons(9734);//要连接到端口号
len = sizeof(address);
result = connect(sockfd,(struct sockaddr *)&address,len);//请求连接
if(result == -1) {
perror("oops:
clientl");
exit(1);
}
write(sockfd,&ch,1);//把数据写入套接字
read(sockfd,&ch,1);//服务器处理完数据后从新读取出来
printf("char form server =
%c
",ch);
close(sockfd);
exit(0);
}
运行这两个程序输出如下:
root@www:/opt/chengxu# ./server2
&
[4] 4746
root@www:/opt/chengxu#
server waiting
root@www:/opt/chengxu# ./client2
& ./client2 & ./client2
&
[5] 4749
[6] 4750
[7] 4751
root@www:/opt/chengxu#
server waiting
server waiting
server waiting
char form server = B
char form server = B
char form server = B
[5]
Done
./client2
[6]-
Done
./client2
[7]+
Done
./client2
root@www:/opt/chengxu#
输出结果基本和上面的例子差不多,通过上面程序可以发现客户端写入到套接字中的数据可以从服务器中读出来,而且客户端会等待服务器把数据读出处理后再把数据读回来,这有一个顺序,并不会出现乱序,有点类型于管道通信而且是双向的
主机字节序和网络字节序
通过套接字接口传递的端口号和地址都是二进制的,不同计算机使用不同的字节序来表示整数,如32位的整数分为4个连续的字节,并以1-2-3-4存在内存中,这里的1表示最高位,也即大端模式,而有的处理器是以4-3-2-1存取的,两个不同的计算机得到的整数就会不一致
为了使不同类型的计算机可以就通过网络传输多字节的值达成一致,客户和服务器程序必须在传出之前,将它们内部整数表示方式转换为网络字节序,它们通过下面函数转换(也就是把端口号等转换成统一网络字节序)
#include
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
这些函数将16位和32位整数在主机字节序和标准的网络字节序之间进行转换,如上面例子中用到的
address.sin_addr.s_addr =
htonl(INADDR_ANY);//要连接到主机IP
address.sin_port = htons(9734);//要连接到端口号
这样就能保证网络字节序的正确,如果你使用的计算机上的主机字节序和网络字节序相同,将看不到任何差异
网络信息
到目前为止,我们客户和服务器程序一直是把地址和端口编译到它们自己的内部,对于一个更通用的服务器和客户程序来说,我们可以通过网络信息函数来决定应该使用的地址和端口(也就是说可以通过网络信息函数来获取IP和端口号等信息)
类似的,如果给定一个计算机名字,你可以通过调用解析地址的主机数据库函数来确定它的IP地址等信息
主机数据库函数在接口文件netdb.h中声明,如下:
#include <netdb.h>
struct hostent *gethostbyaddr(const void *addr,size_t len,int type);
struct hostent *gethostbyname(const char *name);//获得计算机主机数据库信息
这些函数返回的结构体中至少包括以下几个成员
struct hostent {
char *h_name; //name of the host
char **h_aliases;//list of aliases
int h_addrtypr;//address type
int h_length;//length in bytes of the address
char **h_addr_list;//list of address
};
如果没有与我们查询的主机或地址相关的数据项,这些信息函数将返回一个空指针
类型地,与服务及相关联端口号有关的信息也可以通过一些服务信息函数来获取
#include <netdb.h>
struct servent *getservbyname(const char *name,const char *proto);//检查是否有某个服务
struct servent *getservbyport(int port,const char *proto);
proto参数指定用于连接该服务的协议,它的两个选项tcp和udp,前者用于SOCK_STREAM类型
返回结构体中至少包含如下几个成员:
struct servent {
char *s_name;//name of the service
char **s_aliases;//list of aliases
int s_port;//The IP port number
char *s_proto;//The service type,usually "tcp" or "udp"
};
如果想获取某台计算机主机数据库信息,可以调用gethostbyname函数并且将结果打印出来,注意,要把返回的地址表转换为正确的地址类型,并用函数inet_ntoa将它们从网络字节序装换为可打印的字符串,如下:
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
这个函数的作用是将一个因特网主机地址转换为一个点分四元租格式的字符串
下面这个程序getname.c用来获取一台主机有关的信息,如下:
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
char
*host,**names,**addrs;//接收用到的一些指针
struct hostent
*hostinfo;//指向gethostbyname函数返回的结构体指针
if(argc == 1) {
char
myname[256];
gethostname(myname,255);
host
= myname;
}
else
host = argv[1];//获取主机名
hostinfo =
gethostbyname(host);//获取主机数据库
if(!hostinfo) {
fprintf(stderr,"cannot
get info for host: %s
",host);
exit(1);
}
printf("results for host
%s:
",host);
printf("Name: %s
",hostinfo
-> h_name);
printf("Aliases");
names = hostinfo
-> h_aliases;
while(*names) {
printf("
%s",*names);
names++;
}
printf("
");
if(hostinfo
-> h_addrtype != AF_INET) {
fprintf(stderr,"not
an IP host!
");
exit(1);
}
//显示主机的所有IP地址
addrs = hostinfo
-> h_addr_list;
while(*addrs) {
printf("
%s",inet_ntoa(*(struct in_addr *)*addrs));
addrs++;
}
printf("
");
exit(0);
}
运行这个程序输出如下所示:
root@www:/opt/chengxu#
./getname
results for host www:
Name: www.kugoo.com
Aliases www
127.0.1.1
root@www:/opt/chengxu#
./getname localhost
results for host localhost:
Name: localhost
Aliases ip6-localhost ip6-loopback
127.0.0.1 127.0.0.1
root@www:/opt/chengxu#
下面一个例子是连接到标准服务
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc,char
*argv[])//argc表示参数个数,argv[]表示具体参数程序名为argv[0]
{
char *host;
int sockfd;
int len,result;
struct sockaddr_in
address;//套接字地址结构体
struct hostent
*hostinfo;//指向gethostbyname函数返回的结构体指针
struct servent
*servinfo;//指向getservbyname函数返回的结构体指针
char buffer[128];
if(argc == 1)
host = "localhost";
else
host = argv[1];
hostinfo =
gethostbyname(host);//获取主机数据库信息
if(!hostinfo) {
fprintf(stderr,"no
host: %s
",host);
exit(1);
}
//检查主机上是否有daytime服务
servinfo =
getservbyname("daytime","tcp");
if(!servinfo) {
fprintf(stderr,"no
daytime service
");
exit(1);
}
sockfd = socket(AF_INET,SOCK_STREAM,0);//创建一个套接字,并返回一个描述符
address.sin_family =
AF_INET;//使用网络介质
address.sin_port = servinfo
-> s_port;//端口号
address.sin_addr = *(struct
in_addr *)*hostinfo -> h_addr_list;//获取主机IP地址
len = sizeof(address);
result =
connect(sockfd,(struct sockaddr
*)&address,len);//请求连接
if(result == -1) {
perror("oops:getdate");
exit(1);
}
result =
read(sockfd,buffer,sizeof(buffer));
buffer[result] = ' ';
printf("read %d bytes:
%s",result,buffer);
close(sockfd);
exit(0);
}
运行这个程序输出如下:
root@www:/opt/chengxu#
./getname1 localhost
oops:getdate: Connection refused
root@www:/opt/chengxu#
之所以这样是因为我的虚拟机中没有启动daytime这个服务
我用的是ubuntu虚拟机,下面来启动这个daytime服务看一下
首先
root@www:/opt/chengxu# vim
/etc/inetd.conf
进入到inetd的配置文件把这个服务前面的#号去掉,改成如下:
18
#discard
dgram udp
wait
root
internal
19
daytime
stream tcp nowait
root
internal
20
#time
stream tcp nowait
root
internal
然后保存退出
接下来重启一下服务就可以了通过下面命令启动(或重启)xinetd服务(xinetd和openbsd-inetd差不多都是属于守护进程)
root@www:/opt/chengxu#
/etc/init.d/openbsd-inetd restart
* Restarting internet superserver
inetd
[ OK ]
root@www:/opt/chengxu#
可以通过下面命令看一下daytime这个服务是否处于监听状态了,如下:
root@www:/opt/chengxu#
netstat -a | grep daytime
tcp
0
0
*:daytime
*:*
LISTEN
root@www:/opt/chengxu#
下面再重新运行一下上面程序看看:
root@www:/opt/chengxu#
./getname1 localhost
read 26 bytes: Sun Sep 23 23:15:14 2012
root@www:/opt/chengxu#
因特网守护进程
当有客户端连接到某个服务时,守护进程就运行相应的服务器,这使得针对各项网络服务器不需要移植运行着,我们通常是通过一个图形界面来配置xinetd,ubuntu用的好像是openbsd-inetd这 个守护进程,我们可以直接修改它的配置文件,ubuntu对应的是/etc/inetd.conf这个文件,就拿我们上面那个daytime这个服务为 例,在ubuntu中它默认是关闭的,这时我们要进入它的配置文件里,把它前面的#字符去掉就可以了,修改了的配置文件如下:
17 #discard stream tcp nowait root internal
18 #discard dgram udp wait root internal
19 daytime stream tcp nowait root internal
20 #time stream tcp nowait root internal
21
22 #:STANDARD: These are standard services.
23
24 #:BSD: Shell, login, exec and talk are BSD protocols.
25
26 #:MAIL: Mail, news and uucp services.
27
28 #:INFO: Info services
29
30 #:BOOT: TFTP service is provided primarily for booting. Most sites
31 # run this only on machines acting as "boot servers."
32 bootps dgram udp wait root /usr/sbin/bootpd bootpd -i -t 120
33 tftp dgram udp wait nobody /usr/sbin/tcpd /usr/sbin/in.tftpd /srv/tftproot
34 #bootps dgram udp wait root /usr/sbin/bootpd bootpd -i -t 120
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
{
int server_sockfd,client_sockfd;//定义套接字描述符
int server_len,client_len;
struct sockaddr_in server_address;//套接字地址结构体
struct sockaddr_in client_address;//套接字地址结构体
server_address.sin_addr.s_addr = htonl(INADDR_ANY);//要连接到的服务器IP地址
server_address.sin_port = htons(9734);//要连接到的端口号
server_len = sizeof(server_address);
bind(server_sockfd,(struct sockaddr *)&server_address,server_len);//命名套接字
char ch;
printf("server waiting ");
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,(struct sockaddr *)&client_address,&client_len);//接收连接
read(client_sockfd,&ch,1);
sleep(5);
ch++;
write(client_sockfd,&ch,1);
close(client_sockfd);
exit(0);
}
else {
close(client_sockfd);//在父进程中关闭打开的套接字描述符
}
}
}
root@www:/opt/chengxu# ./client2 & ./client2 & ./client2 &
[6] 6742
[7] 6743
[8] 6744
server waiting
root@www:/opt/chengxu# server waiting
server waiting
char form server = B
char form server = B
char form server = B
[7]- Done ./client2
[8]+ Done ./client2
root@www:/opt/chengxu#
上面服务器程序用到了子进程来处理要处理数据,这样一来就可以多个客户连接到服务器了
#include <sys/time.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
{
char buffer[128];
int result,nread;
fd_set inputs,testfds;//定义文件描述符集合
struct timeval timeout;//定义超时结构体
FD_SET(0,&inputs);//把标准输入加到文件描述符集合中
testfds = inputs;
timeout.tv_sec = 2;
timeout.tv_usec = 500000;
switch(result) {
case 0:
printf("timeout ");
break;
case -1:
perror("select");
exit(1);
default:
if(FD_ISSET(0,&testfds)) {//检测是否有标准输入
ioctl(0,FIONREAD,&nread);//得到缓冲区里有多少字节要被读取,存到nread中
if(nread == 0) {
printf("keyboard done ");
exit(0);
}
nread = read(0,buffer,nread);//从标准输入缓冲区中读取数据
buffer[nread] = 0;
printf("read %d from keyboard: %s",nread,buffer);
}
break;
}
}
}
timeout
hello
read 6 from keyboard: hello
timeout
timeout
fread
read 6 from keyboard: fread
keyboard done
root@www:/opt/chengxu#
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
{
int server_sockfd,client_sockfd;//定义套接字描述符
int server_len,client_len;
struct sockaddr_in server_address;//套接字地址结构体
struct sockaddr_in client_address;//套接字地址结构体
int result;
fd_set readfds,testfds;//定义文件描述符集合
server_address.sin_addr.s_addr = htonl(INADDR_ANY);//要连接到服务器的Ip地址
server_address.sin_port = htons(9734);//要连接到的端口号
server_len = sizeof(server_address);
FD_SET(server_sockfd,&readfds);//把server_sockfd描述符加到readfds描述符集合中
char ch;
int fd;
int nread;
result = select(FD_SETSIZE,&testfds,(fd_set *)0,(fd_set *)0,(struct timeval *)0);//检测testfds描述符集合是否有变化,没有的话阻塞等待
perror("server5");
exit(1);
}
if(FD_ISSET(fd,&testfds)) {//检测那个描述符集合发生变化,这个很重要
if(fd == server_sockfd) {//如果是server_sockfd描述符集变化
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,(struct sockaddr *)&client_address,&client_len);//接收连接
FD_SET(client_sockfd,&readfds);//把客户端的client_sockfd描述符加进描述符集合中,这样就可以用select来检测客户端的描述符了
printf("adding client on fd %d ",client_sockfd);
}
ioctl(fd,FIONREAD,&nread);//能得到缓冲区里有多少字节要被读取,存进nread中
close(fd);
FD_CLR(fd,&readfds);
printf("removing client on fd %d ",fd);
}
read(fd,&ch,1);
sleep(50);
printf("serving client on fd %d ",fd);
ch++;
write(fd,&ch,1);
}
}
}
}
}
}
这个程序运行如下:
[7] 7352
[8] 7353
[9] 7354
root@www:/opt/chengxu# server waiting
server waiting
server waiting
char form server = B
char form server = B
char form server = B