Socket是更为一般化的进程间通信方式。
前面讲的IPC都是单机内的。而socket是跨机器的IPC,是管道概念的扩展。
Socket!=RPC
RPC是在Socket的基础上实现的.RPC以模块调用的简单概念忽略通讯细节,让程序员不用关心C/S之间的通讯协议,集中精力对付实现过程,作为普遍的C/S开发方法,开发效率高效。但也决定了RPC生成的通讯包不可能对每种应用都有最恰当的处理办法,它比socket需要更多的网络和系统资源.
这一章简单地介绍一下内容:
Socket如何操作;
Socket属性,地址和通信;
网络信息和Internet精灵 inetd;
服务器和客户端。
什么是Socket?
Socket连接像电话呼叫。
服务器端这边:
首先,1个服务器程序通过系统调用socket创建1个socket连接,这个连接被分配给那个单独的进程独占。
其次,服务器进程通过系统调用bind给socket连接一个名字;等着客户端来连接那个被命名过的socket,系统调用listen会创建1个接收连接的队列;服务器端进程可以通过系统调用accept来接收它们;
当accept调用时,1个新的socket创建了,这个socket不同于被命名过的socket,只用来跟特定的客户端通信。被命名的socket维护着其他的客户端连接,其他客户连接等在listen队列里,直到再次被accept。
客户端那边更加简单直接:
通过socket创建1个未命名的socket,并使用命名的socket作为地址来调用connect设立跟服务器的连接。
一旦创建, socket可以像低级文件描述符一样被使用,提供双向数据通信。
Client端代码:
[fsong@mNode200 temp]$ vi server_socket
#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);
if(result == -1) {
perror("oops: client1");
return (1);
}
write(sockfd, &ch, 1);
read(sockfd, &ch, 1);
printf("char from server = %c\n", ch);
close(sockfd);
return(0);
}
服务器端代码:
#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\n");
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);
}
}
Socket属性
为了全面理解系统调用,你需要了解点UNIX网络。
Socket的3大特征:域,类型,和协议。它们也有个地址作名字,地址格式取决于域,也叫协议族。
Socket domain,又叫family,指定socket通信使用的网络媒体。最普通的域是AF_INET,Linux 局域网 包括因特网本身都是这种类型。承载协议IP有个地址族,即IP地址,它确定了网络上的一个计算机。
在服务器机器上有许多种不同的服务。客户端可以通过IP端口定位到网络计算机上的特定的服务。一些标准的服务有固定的端口号。
AF_UNIX,我们用的第一个域,可以被基于单机环境的而不是网络环境的socket使用。它的名字,而不是IP地址,server_sock,出现在服务器程序运行时的当前目录里。
1个域可能有几种不同的通信方式。每种方式有各自的特征。
比如IP协议提供了Stream和Datagram两种截然不同的服务。
Stream Sockets
顺序的,可靠的,双向的字节流。
发送出去的数据不会乱序,丢失,或重复。
SOCK_STREAM被AF_INET通过TCP/IP连接实现。也是AF_UNIX通常用的类型。
我们将更重视这种方式,因为它是网络编程用的最多的。
Datagram Sockets
相对地,SOCK_DGRAM不设立/维护一个连接;可以发送的报文有大小限制;以单个消息方式法发出;可能乱序,丢失,或重复。
SOCK_DGRAM被AF_INET通过TCP/IP连接实现。因为没有连接建立和维护,所以速度快;服务器的死亡不需要客户端的重起;服务器的启动或停止不会扰乱跟客户端的连接。
要正确选择合适的方式。
#include
#include
int socket(int domain, int type, int protocol);
返回一个文件描述符一样的东西。当与另外一端建立连接后,可以对其进行读写。
domain
AF_UNIX: UNIX Internal(file sytem sockets)
AF_INET: ARPA Internet(UNIX network sockets)
AF_ISO: ISO standard protocols
type
SOCK_STREAM: 流套接字
SOCK_DGRAM: 数据报套接字
SOCK_RAW: 原始报套接字
domain和type可以决定使用的是哪种protocol.
比如,AF_INET+SOCK_STREAM就决定了TCP;
AF_INET+SOCK_DGRAM决定了UDP;
AF_INET+SOCK_RAW决定了IP。
Socket地址
每一个socket域都要求自己的地址格式。
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[]; /* pathname */
};
struct sockaddr_in {
short int sin_family; /* AF_INET */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
};
struct in_addr {
unsigned long int s_addr;
};
命名socket
#include
int bind(int socket, const struct sockaddr *address, size_t address_len);
给文件标识符所代表的未命名socket指派一个地址名字。for AF_UNIX,是一个路径名;对AF_INET,是一个IP地址。INADDR_ANY表示接受本机任何接口来的连接.
成功,返回0;否则,返回-1,并设置errno
EBADF 文件标识符不正确
ENOTSOCK 文件标识符代表的不是socket
EINVAL 文件标识符指向了一个已经命名过的socket
EADDRNOTAVAIL 地址不可用
EADDRINUSE 地址已经被帮定到1个socket了
对AF_UNIX,还有其他错误码
EACCESS 权限问题
ENOTDIR, ENAMETOOLONG
创建socket队列
#include
int listen(int socket, int backlog);
服务器程序创建一个队列储存接入的请求。
成功,返回0,否则返回-1,并设置errno.
backlog 队列的长度
#include
int accept(int socket, struct sockaddr *address, size_t *address_len);
当客户端试图连接时,调用返回。服务器创建1个新socket连接,并返回标识符。新连接跟服务器监听socket有相同的类型。
之前必须被命名过,且有1个监听队列。
呼叫客户端的地址保存在address里。
#include
int connect(int socket, const struct sockaddr *address, size_t address_len);
address保存了想要与之建立连接的服务器地址
主机序和网络序
对2个或2个以上字节的数字来说, 不同的机器有不同的表示顺序.
不管本机上数字顺序如何,在网络上传输的必须是统一规定的网络序(高字节在后),又叫大端序.网络为大?
客户端或服务器端在传输之前需要把本地顺序的多字节整数转换成网络顺序的多字节整数.
转换函数在in.h中定义
#include <netinet/in.h>
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);
例如
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
知道了机器的名字,就能决定机器的IP地址,通过咨询/etc/hosts, 或NIS, 或DNS.
反之亦然.
#include <netdb.h>
struct hostent *gethostbyaddr(const void *addr, size_t len, int type);
struct hostent *gethostbyname(const char *name);
关于hostent:
struct hostent {
char *h_name; /* name of the host */
char **h_aliases; /* list of aliases (nicknames) */
int h_addrtype; /* address type */
int h_length; /* length in bytes of the address */
char **h_addr_list /* list of address (network order) */
};
通过名字或地址也可以获知服务的情况
#include <netdb.h>
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
关于servent:
struct servent {
char *s_name; /* name of the service */
char **s_aliases; /* list of aliases (alternative names) */
int s_port; /* The IP port number */
char *s_proto; /* The service type, usually “tcp” or “udp” */
};
标准的服务器代码:
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
char *host;
int sockfd;
int len, result;
struct sockaddr_in address;
struct hostent *hostinfo;
struct servent *servinfo;
char buffer[128];
if(argc == 1)
host = "localhost";
else
host = argv[1];
hostinfo = gethostbyname(host);
if(!hostinfo) {
fprintf(stderr, "no host: %s\n", host);
exit(1);
}
servinfo = getservbyname("daytime", "tcp");
if(!servinfo) {
fprintf(stderr,"no daytime service\n");
exit(1);
}
printf("daytime port is %d\n", ntohs(servinfo->s_port));
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;
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] = '\0';
printf("read %d bytes: %s", result, buffer);
close(sockfd);
exit(0);
}
inetd( 因特网操作服务程序)
通常,inetd 管理的程序有telnet、ftp、rsh和rlogin。关闭inetd也就 关闭了这些由它管理的服务。
xinetd是inetd的一个替代品,提供更友好的界面。
socket选项
#include
int setsockopt(int socket, int level, int option_name, const void *option_value, size_t option_len);
socket level 是SOL_SOCKET
相应的option包括 SO_DEBUG, SO_KEEPALIVE, option_value是0或1
多个客户端
有客户端连接时,fork一个新的进程,连接该客户断请求,主进程继续接受其他的新的连接。
#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;
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len)
listen(server_sockfd, 5);
signal(SIGCHLD, SIG_IGN);
while(1) {
char ch;
printf(“server waiting\n”);
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, &client_len);
if(fork() == 0) {
read(client_sockfd, &ch, 1);
sleep(5);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
exit(0);
}
else {
close(client_sockfd);
}
}
}
select技术
select系统调用允许程序在许多低级文件描述符上等着输入到达或者输出完成。
这意味这中断模拟程序可以阻塞着直到有事情做。
1个服务器可以处理多个客户端,通过同时在多个打开的socket上等着请求。
fdset是打开的文件描述符的集合,实际上是一个long类型的数组,每一个数组元素都能和一个打开的
文档句柄(不管是Socket句柄,还是其他文档或命名管道或设备句柄)建立联系,建立联系的工作由程序员
完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一
Socket或文档可读。
#include
#include
void FD_ZERO(fd_set *fdset); //把fdset初始化为空集合
void FD_CLR(int fd, fd_set *fdset); //从fdset中去掉fd
void FD_SET(int fd, fd_set *fdset); //向fdset中增加fd
int FD_ISSET(int fd, fd_set *fdset);//判断fd在不在fdset中
struct timeval {
time_t tv_sec;
long tv_usec;
}
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct
timeval *timeout);
select系统调用是用来让我们的程序监视多个文件句柄(文件描述符)的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有某一个或多个发生了状态改变。
ndfs:select监控的文档句柄个数,视进程中打开的文档数而定,一般设为要监控各文档中的最大文档号加1。
select受限于轮询的套接字数量,这个数量也就是系统头文件中定义的FD_SETSIZE(缺省,==64),可以改变,但最大不能超过1024。
readfds, writefds, errorfds 这3个参数都是句柄的集合,第一个rdfds是用来检查可读性的文件句柄的集合,当有句柄的状态变成可读的时系统就会告诉select函数返回,同理第二个
wtfds是指用来检查可写性的文件句柄的集合,有句柄状态变成可写的时系统就会告诉select函数返回,同理第三个参数exfds是特殊情况,即有句柄上发生特殊情况发生时系统会告诉
select函数返回。特殊情况比如对方通过一个socket句柄发来了紧急数据。
timeout 是超时时间。NULL代表无限等待,否则就是最长等待时间。
程序
#include
#include
#include
#include
#include
#include
int main()
{
char buffer[128];
int result, nread;
fd_set inputs, testfds;
struct timeval timeout;
FD_ZERO(&inputs);
FD_SET(0,&inputs);
while(1) {
testfds = inputs;
timeout.tv_sec = 2;
timeout.tv_usec = 500000;
result = select(FD_SETSIZE, &testfds, (fd_set *)NULL, (fd_set *)NULL,
&timeout);
switch(result) {
case 0:
printf("timeout\n");
break;
case -1:
perror("select");
exit(1);
default:
if(FD_ISSET(0,&testfds)) {
ioctl(0,FIONREAD,&nread);
if(nread == 0) {
printf("keyboard done\n");
exit(0);
}
nread = read(0,buffer,nread);
buffer[nread] = 0;
printf("read %d from keyboard: %s", nread, buffer);
}
break;
} //EndOfSwitch
} //EndOfWhile
}
报文
int sendto(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *to, socklen_t tolen);
int recvfrom(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
阅读(1258) | 评论(0) | 转发(0) |