服务器和Socket

4 种类型的数据源
1.磁盘文件
2. 设备
3. 管道
4. Sockets
(1,2) 磁盘 / 设备文件
用 open 命令连接,用 read 和 write 传递数据。
(3) 管道
用 plpe 命令创建,用 foxk 共享,用 read 和 write 传递数据.
(4) Sockets
用 socket 、 listen 和 connect 连接,用 read 和 write 传递数据。
一个客户/服务器模型程序要成为协同系统必须有明确指明消息结束的方法,井且程序必须使用简单并可预测的请求和应答。
fdopen: 让文件描述符像文件一样使用
fdopen 与 fopen 类似,返回一个 FILE*类引用块内容型的值,不同的是此函数以文件描述符而非文件作为参数。

    #include <stdio.h>
    FILE *fopen(const char *path, const char *mode);
    FILE *fdopen(int fildes, const char *mode);
    FILE *freopen(const char *path, const char *mode, FILE *stream);

使用 fopen 的时候,将文件名作为参数传给它。 fopen 可以打开设备文件也可以打开常规的磁盘文件。如只知道文件描述符而不清楚文件名的时候可以使用 fdopen 命令。例如在管道的时候,把一个通向管道的连接转换成 FILE *类型值之后,就可以使用标准缓存的I/ 0 操作来对其进行操作了,使用 fdopen 使得对远端的进程的处理就如同处理常规文件一样。popen函数通过封装 pipe 、 fork 、 dup 和 exec 等系统调用使得对程序和文件的操作变成了一回事。
fopen 打开一个指向文件的带缓存的连接 ,fopen 需要两个宇符串变量作为参数:文件名和连接类型(例如 :”r” 、 “w” 、 “a” 、…)。看上去跟 fopen 很类似。 popen 打开一个指向进程的带缓冲的连接:

FILE *fp;

fp = popen("ls","r");

fgets(buf,len,fp);

pclose(fp);

#include <stdio.h>
FILE *popen(const char *command, const char *type);

popen 和 fopen 之间 的相似性:两者使用相同的语法格式,并具有相同的返回值类型。popen 的第一个参数是要打开的命令的名称;它可以是任意的 shell 命令。第二个参数可以是 "r" 或 "w" 。

popen()函数通过创建管道,调用shell来打开一个进程。由于管道的定义是单向的,所以类型参数可以只指定读或写,而不是两者(半双工);结果流是相应的只读或只写。

command参数是一个指向包含shell命令行的以null结尾的字符串的指针。该命令使用-c标志传递给/bin/sh;如果有的话,解释是由shell执行的。

type参数是一个指向null结束字符串的指针,该字符串必须包含读取的字母'r'或写入的字母'w'。由于glibc 2.9,此参数可以另外包含字母“e”,这会导致在底层文件描述符上设置关闭执行标志(FD_CLOEXEC);请参阅open中O_CLOEXEC标志的描述。

popen()的返回值在所有方面都是标准的I/O流,除了它必须用pclose()而不是fclose来关闭。写入这样的流相当于写入命令的标准输入;该命令的标准输出与调用 popen()的进程的标准输出相同,除非这个命令本身被改变了。请注意,输出popen()流默认是块缓冲。pclose()函数等待关联的进程终止并返回该命令的退出状态由wait4返回。

popen 运行了一个程序并返回指向该程序标准输入或标准输出的连接。
需要一个新的进程来运行程序,所以要用到 fork 命令。
需要一个指向该进程的连接(管道)。并且使用 fdopen 命令将一个文件描述符定向到缓冲流中。惟一能够运行任意 shell 命令的程序是 shell 本身即 /bin/sh 。为了使程序员可以方便的使用, sh 支持-c 选项,用以告诉 shell 执行某命令然后退出。

#include<stdio.h>

#include<signal.h>

#include<unistd.h>

#include<stdlib.h>

#define READ 0

#define WRITE 1

FILE *popen(const char *command,const char *mode)

{

    int pfp[2],pid;

    FILE *fp;

    int parent_end,child_end;

    if(*mode == 'r')

    {

        parent_end = READ;

        child_end = WRITE;

    }

    else if(*mode == 'w')

    {

        parent_end = WRITE;

        child_end = READ;

    }else return NULL;

    if(pipe(pfp) == -1)

        return NULL;

    if((pid = fork()) == -1)

    {

        close(pfp[0]);

        close(pfp[1]);

        return NULL;

    }

    if(pid > 0)

    {

        if(close(pfp[child_end]) == -1)

            return NULL;

        return fdopen(pfp[parent_end],mode);

    }

    if(close(pfp[child_end]) == -1)

        exit(1);

    if(dup2(pfp[child_end],child_end) == -1)

        exit(1);

    if(close(pfp[child_end]) == -1)

        exit(1);

    execl("/bin/sh","sh",command,NULL);

        exit(1);

    }

访问数据:文件、应用程序接口 (API) 和服务器
fopen 从文件获得数据,而 popen 从进程获得数据。
1 :从文件获取数据
可以通过读取文件来获取数据。基于文件的信息服务并不是很完美。客户端程序依赖于特定的文件格式和结构体中的特定成员名称。
2 :从函数获取数据
可以通过调用函数来得到数据 。一 个库函数用标准的函数接口来封装数据的格式和位置。使用基于应用程序接口 (API) 的信息服务不一定是最好的方法。有两种方法可以使用库函数 。一 个程序可以使用静态连接来包含实际的函数代码。但是这些函数有可能包含的并不是正确的文件名或文件格式 。 另一方面,一个程序可以调用共享库中的函数,但是这些共享库也并不是安装在所有的系统上,或者其版本并不是程序所要使用的版本 。
3. 从进程获取数据
可以用管道使进程的标准输出重定向获取数据。由进程来负责使用正确的文件名和文件格式以及正确的库函数,而不是你的程序。调用独立的程序获得数据还有其他的好处 。 服务器程序可以使用任何程序设计语言编写。以独立程序的方式实现系统服务的最大好处是客户端程序和服务器端程序可以运行在不同的机器上,所有要做的只是和不同机器上的一个进程相连接 。
前面所说管道使得进程向其他进程发送数据就像向文件差送数据一样容易,但是这个管道具有两个重大的缺陷。此管道在一个进程中被创建,通过 fork 来实现共享 。 因此,此管道只能连接相关的进程,也只能连接同一台主机上的进程。这种管道叫匿名管道。
还有一种管道叫命名管道:
有名管道是对无名管道的一种改进,特性如下:
1. 它可以使互不相关的两个进程间实现彼此通信;
2. 该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当做普通文件一样进行读写操作,使用非常方便;
3. FIFO 严格地遵循先进先出规则,对管道及 FIFO 的读总是从开始处返
回数据,对它们的写则是把数据添加到末尾,它们不支持如 lseek()等文件定位操作
命名管道使用的函数:

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
#include <fcntl.h>           /* Definition of AT_* constants */
   #include <sys/stat.h>

   int mkfifoat(int dirfd, const char *pathname, mode_t mode);

mkfifo()生成一个名为pathname的FIFO特殊文件。mode指定了FIFO的权限。它被修改通过进程的umask以通常的方式:创建的文件的权限(mode&umask)。

FIFO使用文件与管道相似,只是它以不同的方式创建。而不是一个匿名通信通道,通过调用mkfifo()将FIFO特殊文件输入到文件系统中。一旦以这种方式创建了一个FIFO专用文件,任何进程都可以打开它进行读写与普通文件相同的方式。

必须在两端同时开放,然后才能继续对其进行任何输入或输出操作。打开一个FIFO读取正常块,直到其他进程打开相同的FIFO进行写入,反之亦然。参阅fifo了解FIFO特殊的非阻塞处理文件。

mkfifoat()函数的运行方式与mkfifo()完全相同,除了描述的差异。如果路径名中给出的路径名是相对的,那么它是相对于所引用的目录进行解释的文件描述符   dirfd(而不是相对于调用进程的当前工作目录,就像由mkfifo()完成相对路径名)。如果路径名是相对的而dirfd是特殊值AT_FDCWD,那么路径名就相对于该路径被解释调用进程的当前工作目录(如mkfifo())。如果路径名是绝对的,那么dirfd被忽略。

Unix 提供了另外一种进程间的通信机制
一、socket 。
socket 允许在不相关的进程间创建类似管道的连接,甚至可以通过 socket 连接其他主机上的进程。
socket用于连接到远端的进程以获取服务。
1、建立服务及与服务相关的操作(服务器端)
(1)建立服务
a.需要和远方的通信端点。
b.需要被连接的地址
c.处理链接请求。
(2) 与服务相关的操作
包含等待接入(阻塞的)、提供服务、断开连接三个步骤的循环。
2、使用服务(客户端)
(1)建立连接
a.需要和远方的通信端点
b.需要请求连接的地址
c.使用服务
c.断开连接
3、重要概念
连接远程的4个重要概念:
(1)客户和服务器
服务器是提供服务的程序。服务器进程等待请求,处理请求,然后循环回去等待下一个请求。客户端进程则不需要等待,只需要建立一个链接,与服务器交换数据,然后继续自己的工作。
(2)主机名与端口号
运行于网络上的服务器其实是某台计算机上(主机)运行的一个进程。机器的名字称为主机名,服务器在该主机上拥有一个端口。主机和端口的结合才标识了一个服务器。
(3)地址族
一类地址的集合
(4)协议
协议是服务器和客户之间交互的规则,每个客户/服务器模型都必须定义这样的协议。

服务列表:(端口号)
可以在/etc/services查看服务端口号。
这里写图片描述
一个请求时间服务器:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netdb.h>
#include<time.h>
#include<string.h>
#include<netinet/in.h>
#include<stdlib.h>
#define PORTNUM 13000
#define HOSTLEN 256
#define oops(msg) {perror(msg);exit(1);}

int main(int argc,char *argv)
{
    struct sockaddr_in saddr;
    int sock_id,sock_fd;
    FILE *sock_fp;
    char *ctime(const time_t*);
    time_t thetime;

    sock_id = socket(PF_INET,SOCK_STREAM,0);
    if(sock_id == -1)
        oops("socket creat error!\n");
    saddr.sin_port = htons(PORTNUM);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = inet_addr("IP");

    if(bind(sock_id,(struct sockaddr*)&saddr,sizeof(saddr))!=0)
        oops("bind error!");
    if(listen(sock_id,1)!=0)
        oops("listen error");
    while(1)
    {
        sock_fd = accept(sock_id,NULL,NULL);
        printf("Wow!got a call!\n");
        if(sock_fd == -1)
          oops("accept error");
        sock_fp = fdopen(sock_fd,"w");
        if(sock_fp == NULL)
          oops("fdopen error!");
        thetime = time(NULL);
        fprintf(sock_fp,"The thim here is..");
        fprintf(sock_fp,"%s",ctime(&thetime));
        fclose(sock_fp);
      }

    }

建立服务器端步骤:
(1).向内核申请一个socket

#include <sys/socket.h>
mysocket = socket(int socket_family, int socket_type, int protocol);
(man 2 socket 参数查看详情)

socket 调用创建一个通信端点并返回一个标识符。有很多种类型的通信系统,每个被称为一个通信域。

socket 的类型指出了程序将要使用的数据流类型。 SOCK_STRAEAM 类型跟双向的管道类似。数据作为连续的字节流从一端写入,再从另一端读出

函数中最后的参数 protocol 指的是内核中网络代码所使用的协议,并不是客户端和服务器之间的协议。一个为 0 的值代表选择标准的协议。

(2).绑定地址到 socket 上,地址包括主机、端口

#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

bind 调用把一个地址分配给 socket,当进程要与服务器连接的时候,它们就使用该地址。每个地址族都有自己的格式。因特网地址族 (AF_INET) 使用主机和端口来标志。地址就是一个以主机和端口为成员的结构体。自己写的程序应首先初始化该结构的成员,然后再填充具体的主机地址和端口号,最后填充地址族

(3).在 socket 上,允许接入呼叫并设置队列长度为 1
服务器接收接入的呼叫,所以这里的程序必须使用 listen 。

#include <sys/socket.h>

int listen(int s, int backlog);

listen 请求内核允许指定的 socket 接收接入呼叫。并不是所用类型的 socket 都能接收接入呼叫。但 SOCK_STREAM 类型是可以的。第二个参数指出接收队列的长度。队列最大长度则取决于具体 socket 的实现。
(4).等待/接收呼叫
一但socket 被建立并被分配一个地址,而且准备等待接收呼叫,程序即将开始工作。服务器等待直到呼叫到来。它使用系统调用 accept 来接收调用。

 #include <sys/types.h>
 #include <sys/socket.h>

 int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

accept 阻塞当前进程,一直到指定 socket 上的接入连接被建立起来,然后 accept 将返回文件描述符,并用该文件描述符来进行读写操作。此文件描述符实际上是连到呼叫进程的某个文件描述符的一个连接。

accept 支持一种类型的呼叫者的 ID 。在呼叫发起者一边, socket 有自己的地址,如果addr和addrlen指针不为空的话,内核将把呼叫者地址填充到 addr 所指向的结构中,并把该结构的长度填充到 addrlen 所指向的内存单元中。一个网络程序可以使用呼叫进程的地址来决定如何处理该连接。

(5),传输数据
accept 调用所返回的文件描述符是一个普通文件的描述符
(6).关闭连接
accept 所返回的文件描述符可以由标准的系统调用 close 关闭。当一端的进程关闭了该端的 socket ,若另一端的进程在试图读数据的话,它将得到文件结束标记。这跟管道的工作原理类似。
客户端:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
#include<arpa/inet.h>

#define oops(msg) {perror(msg);exit(1);}

int main(int argc,char *argv[])
{
  struct sockaddr_in servadd;
  int sock_id;
  char message[BUFSIZ];
  int messlen;

  sock_id = socket(AF_INET,SOCK_STREAM,0);
  if(sock_id < 1)
    oops("socket error");
  servadd.sin_port = htons((uint16_t)atoi(argv[2]));
  servadd.sin_family = AF_INET;
  servadd.sin_addr.s_addr = inet_addr("192.168.3.119");
  if(connect(sock_id,(structsockaddr*)&servadd,sizeof(servadd))!=0)
    oops("connect");
  messlen = (int)read(sock_id,message,BUFSIZ);
  if(messlen == 1)
    oops("read");
  if(write(1,message,(size_t)messlen)!= messlen)
    oops("write");
  close(sock_id);
}   

1. 向内核请求建立 socket
客户端需要一个 socket 跟网络相连
客户端必须建立 Internet 域( AF _ INET) socket ,并且它还必须是流 socket
( SOCK_STREAM)。
2 :与服务器相连
客户端需要连接到时间服务器。

 #include <sys/types.h>          /* See NOTES */
 #include <sys/socket.h>
 int connect(int sockfd, const struct sockaddr *addr,
               socklen_t addrlen);

connect 调用试图把由 sockfd 所标识的 socket 和由 serv_addr 所指向的 socket 地址相连接。如果连接成功的话, connect 返回 0 。而此时, socfid 是一个合法的文件描述符,可以用来进行读写操作。写入该文件描述符的数据被发送到连接的另-端的 socket ,而从另一端写入的数据将从该文件描述符读取。
3 和 4: 传送数据和挂断
在成功连接之后,进程可以从该文件描述符读写数据,就像与普通的文件或管道相连接一样。若客户端退出而不关闭描述符,内核将完成关闭文件描述符的任务 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值