diakla

导航

网络编程

预备

文件描述符

一个进程在此存在期间,会有一些文件被打开,从而会返回一些文件描述符,从shell中运行一个进程,默认会有3个文件描述符存在(0、1、2),0与进程的标准输入相关联,1与进程的标准输出相关联,2与进程的标准错误输出相关联,一个进程当前有哪些打开的文件描述符可以通过/proc/进程ID/fd目录查看。 

lseek()移动文件的读写位置

  • 头:include <sys/types.h> #include <unistd.h>

  • 定义:off_t lseek(int fildes, off_t offset, int whence);

    参数fildes 为已打开的文件描述词, 参数offset 为根据参数whence来移动读写位置的位移数.

    参数 whence 为下列其中一种:

    • ​ SEEK_SET 参数offset 即为新的读写位置.
    • ​ SEEK_CUR 以目前的读写位置往后增加offset 个位移量.
    • ​ SEEK_END 将读写位置指向文件尾后再增加offset 个位移量. 当whence 值为SEEK_CUR 或
    • ​ SEEK_END 时, 参数offet 允许负值的出现.
  • 返回值:当调用成功时则返回目前的读写位置, 也就是距离文件开头多少个字节. 若有错误则返回-1, errno 会存放错误代码.

fseek()移动文件流的读写位置

  • 头:include <stdio.h>

  • 定义:int fseek(FILE * stream, long offset, int whence);

    参数stream 为已打开的文件指针 ,参数offset 为根据参数whence 来移动读写位置的位移数。参数 whence 为下列其中一种:

    SEEK_SET 从距文件开头offset 位移量为新的读写位置.

    SEEK_CUR 以目前的读写位置往后增加offset 个位移量.

    SEEK_END 将读写位置指向文件尾后再增加offset 个位移量. 当whence 值为SEEK_CUR 或 SEEK_END 时, 参数offset 允许负值的出现.

dup/dup2函数

  • 复制一个现存的文件描述符。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
  • 当调用dup函数时,内核在进程中创建一个新的文件描述符,此描述符是当前可用文件描述符的最小数值,这个文件描述符指向oldfd所拥有的文件表项。   
  • dup2和dup的区别就是可以用newfd参数指定新描述符的数值,如果newfd已经打开,则先将其关闭。如果newfd等于oldfd,则dup2返回newfd, 而不关闭它。dup2函数返回的新文件描述符同样与参数oldfd共享同一文件表项。   
  • APUE用另外一个种方法说明了这个问题:   实际上,调用dup(oldfd)等效于,fcntl(oldfd, F_DUPFD, 0)   而调用dup2(oldfd, newfd)等效于,close(oldfd);fcntl(oldfd, F_DUPFD, newfd);

fcntl根据文件描述词来操作文件的特性

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);         
int fcntl(int fd, int cmd, struct flock *lock);
  • fcntl()针对(文件)描述符提供控制.参数fd是被参数cmd操作(如下面的描述)的描述符.
  • 针对cmd的值,fcntl能够接受第三个参数(arg)
  • fcntl函数有5种功能:

   1.复制一个现有的描述符(cmd=F_DUPFD).

   2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).

​ 3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).

​ 4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).

​ 5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

wait

#include <sys/types.h>
#include <wait.h>
int wait(int *status)
  • 父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

  • wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.

  • 当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程

  • 如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态

  • 参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:

    pid = wait(NULL);

    如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

waitpid

#include <sys/types.h> 
#include <sys/wait.h> 
pid_t waitpid(pid_t pid,int *status,int options)
  • 从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。

  • pid

    从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。

    1. pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
    2. pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
    3. pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
    4. pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
  • options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);如果使用了WNOHANG(wait no hung)参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。

execl

#include<unistd.h>
int execl(const char * path, const char * arg, ...);
  • execl()用来执行参数path 字符串所代表的文件路径, 接下来的参数代表执行该文件时传递过去的argv(0),argv[1], ..., 最后一个参数必须用空指针(NULL)作结束.

常见的通讯方式

1.管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2.命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
3.消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
4.共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
5.信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
6.套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
7.信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

按通信类型区分

  • 共享存储器系统

    1.基于共享数据结构的通信方式
    (仅适用于传递相对少量的数据,通信效率低,属于低级通信)
    2.基于共享存储区的通信方式

  • 管道通信系统

    管道是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件(pipe文件)
    管道机制需要提供一下几点的协调能力
    1.互斥,即当一个进程正在对pipe执行读/写操作时,其它进程必须等待
    2.同步,当一个进程将一定数量的数据写入,然后就去睡眠等待,直到读进程将数据取走,再去唤醒。读进程与之类似
    3.确定对方是否存在

  • 消息传递系统

    1.直接通信方式
    发送进程利用OS所提供的发送原语直接把消息发给目标进程
    2.间接通信方式
    发送和接收进程都通过共享实体(邮箱)的方式进行消息的发送和接收

  • 客户机服务器系统

    1.套接字 – 通信标识型的数据结构是进程通信和网络通信的基本构件
    基于文件型的 (当通信进程都在同一台服务器中)其原理类似于管道
    基于网络型的(非对称方式通信,发送者需要提供接收者命名。通信双方的进程运行在不同主机环境下被分配了一对套接字,一个属于发送进程,一个属于接收进程)
    2.远程过程调用和远程方法调用

管道

把一个进程连接到另一个进程的一个数据流称为一个“管道”,通常是用作把一个进程的输出通过管道连接到另一个进程的输入。管道本质上是内核的一块缓存。

FIFO

FIFO有时被称为命名管道。管道只能由相关进程使用,这些相关进程的共同祖先进程创建了管道。但是,通过FIFO,不相关的进程也能交换数据。

FIFO是一种文件类型。stat结构成员st_mode的编码指明文件是否是FIFO类型。可以用S_ISFIFO()宏对此进行测试。

#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
返回值:若成功则返回0,若出错则返回-1

FIFO有下面两种用途:

(1)FIFO由shell命令使用以便将数据从一条管道线传送到另一条,为此无需创建中间临时文件。

(2)FIFO用于客户进程-服务器进程应用程序中,以在客户进程和服务器进程之间传送数据。

mmap内存映射区

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr:允许用户将某个特定的地址设为这段内存的起始地址,设为NULL表示系统自动分配一个地址
prot:访问权限PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE
fd:被映射的文件对应的文件描述符
offset:设置从文件的何时开始映射
flags:控制内存段内容被修改后程序的行为

信号量

  • 信号的三种状态:产生、未决、递达
  • 信号的处理方式:默认动作、忽略、捕捉
  • 阻塞信号集未决信号集
  • 信号四要素:
    • 编号
    • 名称
    • 事件
    • 处理动作

signal

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  1. signum:指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。  
  2. handler:描述了与信号关联的动作,它可以取以下三种值:
    • SIG_IGN: 忽略该信号
    • SIG_DFL: 恢复对信号的系统默认处理 不写此处理函数默认也是执行系统默认操作
    • sighandler_t类型的函数指针: 接收到一个类型为sig的信号时,就执行handler 所指定的函数

返回值:返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。

signal()会依参数signum 指定的信号编号来设置该信号的处理函数。当指定的信号到达时就会跳转到参数handler指定的函数执行。 当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用相应的处理函数。但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中断。

kill

#include<stdio.h>    //printf()
#include<stdlib.h>    //exit()
#include<unistd.h>    //fork()
#include<signal.h>    
  
int main(int argc, char const *argv[])
{
    if(argc != 2)
    {
        printf("Please input pid\n");
        return 0;
    }
    int pid =1;
    sscanf(argv[1], "%d",  &pid);
    if(pid > 0)
    {
        kill(pid, SIGINT);
    }
    return 0;
}

功能

向进程或进程组发送一个信号 (成功返回 0; 否则,返回 -1 )

参数说明

pid:接收信号的进程(组)的进程号

pid>0:发送给进程号为pid的进程

pid=0:发送给当前进程所属进程组里的所有进程

pid=-1:发送给除1号进程和自身以外的所有进程

pid<-1:发送给属于进程组-pid的所有进程

signo:发送的信号值

abort()

C 库函数 void abort(void) 中止程序执行,直接从调用的地方跳出。

#include <stdio.h>
#include <stdlib.h>

int main ()
{
   FILE *fp;
   
   printf("准备打开 nofile.txt\n");
   fp = fopen( "nofile.txt","r" );
   if(fp == NULL)
   {
      printf("准备终止程序\n");
      abort();
   }
   printf("准备关闭 nofile.txt\n");
   fclose(fp);
   
   return(0);
}

settimeer

SetTimer是一种API函数,位于user32.dll中。你想每隔一段时间执行一件事的的时候,你可以使用它。 使用定时器的方法比较简单,通常告诉Windows一个时间间隔,然后Windows以此时间间隔周期性触发程序。通常有两种方法来实现:发送WM_TIMER消息和调用应用程序定义的回调函数。不需要指定定时器时,可以调用对应的KillTimer函数销毁指定的时钟。

#include <sys/time.h>
struct itimerval {
    struct timeval it_interval; /* next value */
    struct timeval it_value;    /* current value */
};
 
struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
  1. which
    • ITIMER_REAL:以系统真实的时间来计算,它送出SIGALRM信号。
    • ITIMER_VIRTUAL:以该进程在用户态下花费的时间来计算,它送出SIGVTALRM信号。
    • ITIMER_PROF:以该进程在用户态下和内核态下所费的时间来计算,它送出SIGPROF信号。
  2. new_value:先对it_value倒计时,当it_value为零时触发信号,然后重置为it_interval,继续对it_value倒计时,一直这样循环下去。基于此机制,setitimer既可以用来延时执行,也可定时执行。 假如it_value为0是不会触发信号的,所以要能触发信号,it_value得大于0;如果it_interval为零,只会延时,不会定时(也就是说只会触发一次信号)。
  3. old_value:通常用不上,设置为NULL,它是用来存储上一次setitimer调用时设置的new_value值

sigaction

sigaction()会依参数signum 指定的信号编号来设置该信号的处理函数. 参数signum 可以指定SIGKILL 和SIGSTOP 以外的所有信号

#include <signal.h>
struct sigaction
{
    void (*sa_handler) (int);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer) (void);
}
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  1. sa_handler 此参数和signal()的参数handler 相同, 代表新的信号处理函数, 其他意义请参考signal().
  2. sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号搁置.
  3. sa_restorer 此参数没有使用.
  4. sa_flags 用来设置信号处理的其他相关操作, 下列的数值可用:
    • A_NOCLDSTOP: 如果参数signum 为SIGCHLD, 则当子进程暂停时并不会通知父进程
    • SA_ONESHOT/SA_RESETHAND: 当调用新的信号处理函数前, 将此信号处理方式改为系统预设的方式.
    • SA_RESTART: 被信号中断的系统调用会自行重启
    • SA_NOMASK/SA_NODEFER: 在处理此信号未结束前不理会此信号的再次到来. 如果参数oldact 不是NULL 指针, 则原来的信号处理方式会由此结构sigaction 返回

返回值:执行成功则返回0, 如果有错误则返回-1

sigchld

产生sigchld函数的条件

  • 子进程结束
  • 子进程收到sigstop
  • 子进程停止时收到了sigcont信号

线程

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

pthread_create

创建线程(实际上就是确定调用该线程函数的入口点),在线程创建以后,就开始运行相关的线程函数

#include <pthread.h>
int  pthread_create(pthread_t  *thread,  pthread_attr_t  *attr,  void  *(*start_routine)(void  *),  void  *arg)
  1. thread:线程标识符;
  2. attr:线程属性设置;
  3. start_routine:线程函数的起始地址;
  4. arg:传递给start_routine的参数;

返回值:成功,返回0;出错,返回-1。

编译链接参数:-lpthread

pthread_exit

使用函数pthread_exit退出线程,这是线程的主动行为。线程的终止可以是调用了pthread_exit或者该线程的例程结束。也就是说,一个线程可以隐式的退出,也可以显式的调用pthread_exit函数来退出。 由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放,但是可以用pthread_join()函数来同步并释放资源。

#include <pthread.h>
void pthread_exit( void * value_ptr );
  1. value_ptr是函数的返回代码,只要pthread_join中的第二个参数value_ptr不是NULL,这个值将被传递给value_ptr。

编译链接参数:-lpthread

pthread_join

以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
  1. thread: 线程标识符,即线程ID,标识唯一线程。
  2. retval: 用户定义的指针,用来存储被等待线程的返回值

返回值 : 0代表成功。 失败,返回的则是错误号

编译链接参数:*lpthread

网络编程

协议

协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。它的三要素是:语法、语义、时序。
为了使数据在网络上从源到达目的,网络通信的参与方必须遵循相同的规则,这套规则称为协议(protocol),它最终体现为在网络上传输的数据包的格式。

OSI

国际标准化组织(ISO)为了规范协议层次的划分制定了开发系统互联(OSI,Open Systems Interconnection)模型,即ISO/OSI参考模型。此模型根据网络功能制定出7层网络协议结构,由低到高分别为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层
(1)物理层:简单地说,物理层协议对与基本物理信号传输有关的机械、电气等功能进行描述。若生产相互连接的两个设备的两个厂商都遵循相同物理层规范,则二者必定能被连接在一起,并能接收对方发来的电、光或其他的物理信号,而且能正确地将这些物理信号理解为二进制的0和1序列。物理层只负责正确地接收和发送比特,并不关心这些比特的具体含义。
(2)数据链路层:数据链路层简称链路层,它依赖物理层提供的比特传输能力把数据组织成为有边界的传输单位,称为“帧”。链路层把来自网络层的数据组织成“帧”,然后再通过物理层向外发送。当然,链路层也要负责从来自物理层的比特序列(或者字节序列)中区分出一个个的帧,并将帧中的数据传递给网络层。为了将各个帧区分开来,需要在帧的头部和尾部附加一些特点的信息,这个过程称为“封装”,其相反的过程称为“解封装”。“封装”的概念不只在链路层中存在,在更高的各层协议中同样存在。所有层上的“封装”问题的共同特征是把来自高层的封装单位根据本层的需要附加上特定信息形成本层的封装单位,然后向低层传递,同时把来自低层的数据解封装后向高层传递。另外,链路层还可以有其他的诸如差错校验、流量控制等功能,但要理解整个协议体系,则首先应记住它和帧之间的密切关系,因为帧使无头无尾的比特序列变成容易控制的有界单位。
(3)网络层:网络层解决如何标识通信各方和数据如何从源到达目的这个问题。网络层用特定的网络层地址来标识整个网络中的一个节点,并负责使来自传输层的应该到达某个网络层地址的数据能够被送达这个网络层地址所对应的网络节点。网络层的封装单位称为“包”,“包”需要被进一步封装成链路层的帧然后才能通过物理层发送出去,而在接收方,包在链路层的帧中被解封装出来。最典型的的网络层协议就是在Internet中使用的IP协议,它使用IP地址唯一地标识Internet中的一台主机,路由设备根据IP包中的目的IP地址将IP包一步步转发至目的主机。
(4)传输层:依赖物理层、数据链路层和网络层,任意一个网络节点都能把任何信息传递到其他任意节点,而传输层在物理层、数据链路层和网络层提供的节点间的通信能力基础上进一步提供了面向应用的服务。传输层向上层提供屏蔽了传输细节的数据传输服务,将来自高层的数据进行分段并将来自低层的数据重组,对数据传输进行差错恢复和流量控制。通过对每个网络节点的多个进程进行标识,传输层可以实现对网络层的多路复用。
(5)会话层:会话层用于建立和管理不同主机的两个进程之间的对话。会话层可以管理对话,可允许对话在两个方向上同时进行,也可以强制对话同时只在一个方向上进行。在后一种情况下,会话层可以提供会话令牌来控制某时刻哪一方可以发生数据。会话层还可以提供同步服务,它可以在数据流中插入同步点,每当因网络出现故障而造成大量数据传输中断时,通过同步点机制可以使两个进程之间的数据传输不需要从头开始,而是从最后一个同步点开始继续传输。
(6)表示层:表示层协议规定对来自应用层的数据如何进行表达,例如采用什么样的文字编码、是否及如何进行压缩、是否及如何加密等。
(7)应用层:应用层是ISO/OSI模型中最靠近用户的一层,应用层协议直接面对用户的需求,例如与发送邮件相关的应用层协议可以规定诸如邮件地址的格式、邮件内容的段落表示、客户与服务器进行交互的命令串等。

TPC/IP

TCP/IP协议毫无疑问是这三协议中最重要的一个,作为互联网的基础协议,没有它就根本不可能上网,任何和互联网有关的操作都离不开TCP/IP协议。不过TCP/IP协议也是这三大协议中配置起来最麻烦的一个,单机上网还好,而通过局域网访问互联网的话,就要详细设置IP地址,网关,子网掩码,DNS服务器等参数。
TCP/IP尽管是最流行的网络协议,但TCP/IP协议在局域网中的通信效率并不高,使用它在浏览“网上邻居”中的计算机时,经常会出现不能正常浏览的现象。此时安装NetBEUI协议就会解决这个问题。

网络通信方式

CS:Client/Server,客户-服务器方式
BS:Browser/Server,浏览器-服务器方式
P2P:peer to peer,对等方式
BS其实是CS方式的一种特例,所以也应算在CS中。
CS:主机A如果运行客户端程序,而主机B运行服务端程序,客户A向服务端B发送请求服务,服务器B向客户A接收服务,这种情况下,就是以CS的方式进行通信。我们所指的客户和服务器都是值通信中涉及的两个应用进程,而不是具体的主机。
P2P:以对等方式进行通信,并不区分客户端和服务端,而是平等关系进行通信。在对等方式下,可以把每个相连的主机当成既是主机又是客户,可以互相下载对方的共享文件。比如迅雷下载就是典型的p2p通信方式。
BS和CS通信的实质相同,都是客户端向服务器端发送请求,服务端接收并处理。但是BS相对于CS来说更方便,对电脑配置要求更低,并且易于维护,安全性在某种意义上要好些,CS中容易被反汇编,但是CS对于那种复杂的业务处理要更容易一些。

以太网帧格式

在以太网链路上的数据包称作以太帧。以太帧起始部分由前导码和帧开始符组成。后面紧跟着一个以太网报头,以MAC地址说明目的地址和源地址。帧的中部是该帧负载的包含其他协议报头的数据包(例如IP协议)。以太帧由一个32位冗余校验码结尾。它用于检验数据传输是否出现损坏。

常见协议对应的端口号

UDP DHCP服务器端:67,

DHCP客户端:68,DNS服务:53

TCP POP3(邮件接收协议):110

SMTP(邮件传输协议):25,HTTP服务:80

TCP FTP:数据传输为20,控制命令传输为21,Telnet:23

端口号范围为:165535,11024为熟知端口号,1025~65535称为动态端口

socket

这个函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。

#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
  • domain

函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义。

PF_UNIX,PF_LOCAL 本地通信 PF_X25 ITU-T X25 / ISO-8208协议
AF_INET,PF_INET IPv4 Internet协议 PF_AX25 Amateur radio AX.25
PF_INET6 IPv6 Internet协议 PF_ATMPVC 原始ATM PVC访问
PF_IPX IPX-Novell协议 PF_APPLETALK Appletalk
PF_NETLINK 内核用户界面设备 PF_PACKET 底层包访问

  • type

函数socket()的参数type用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字)、SOCK——DGRAM(数据包套接字)等。

SOCK_STREAM Tcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输
SOCK_DGRAM 支持UDP连接(无连接状态的消息)
SOCK_SEQPACKET 序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出
SOCK_RAW RAW类型,提供原始网络协议访问
SOCK_RDM 提供可靠的数据报文,不过可能数据会有乱序
SOCK_PACKET 这是一个专用类型,不能呢过在通用程序中使用
并不是所有的协议族都实现了这些协议类型,例如,AF_INET协议族就没有实现SOCK_SEQPACKET协议类型。

  • protocol

函数socket()的第3个参数protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。

类型为SOCK_STREAM的套接字表示一个双向的字节流,与管道类似。流式的套接字在进行数据收发之前必须已经连接,连接使用connect()函数进行。一旦连接,可以使用read()或者write()函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接收,当数据在一段时间内任然没有接受完毕,可以将这个连接人为已经死掉。
SOCK_DGRAM和SOCK_RAW 这个两种套接字可以使用函数sendto()来发送数据,使用recvfrom()函数接受数据,recvfrom()接受来自制定IP地址的发送方的数据。
SOCK_PACKET是一种专用的数据包,它直接从设备驱动接受数据。
errno
函数socket()并不总是执行成功,有可能会出现错误,错误的产生有多种原因,可以通过errno获得:

EACCES 没有权限建立制定的domain的type的socket
EAFNOSUPPORT 不支持所给的地址类型
EINVAL 不支持此协议或者协议不可用
EMFILE 进程文件表溢出
ENFILE 已经达到系统允许打开的文件数量,打开文件过多
ENOBUFS/ENOMEM 内存不足。socket只有到资源足够或者有进程释放内存
EPROTONOSUPPORT 制定的协议type在domain中不存在

bind

bind函数把一个本地协议地址赋予一个套接字。对于网际协议,协议地址是32位的IPv4地址或是128位的IPv6地址与16位的TCP或UDP端口号的组合。

#include<sys/socket.h>

int bind(int sockfd,  const struct sockaddr, socklen_t addrlen);

第二个参数是一个指向特定协议的地址结构的指针,第三个参数是该地址结构的长度。对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。

listen

listen在套接字函数中表示让一个套接字处于监听到来的连接请求的状态listen在套接字函数中表示让一个套接字处于监听到来的连接请求的状态

#include<sys/socket.h>
int listen(int sockfd, int backlog);

sockfd 一个已绑定未被连接的套接字描述符
backlog 连接请求队列(queue of pending connections)的最大长度(一般由2到4)。用SOMAXCONN则为系统给出的最大值

accept

WINSOCK_API_LINKAGE
SOCKET
WSAAPI
accept(
    SOCKET s,
    struct sockaddr FAR * addr,
    int FAR * addrlen
    );

函数的第一个参数用来标识服务端套接字(也就是listen函数中设置为监听状态的套接字),第二个参数是用来保存客户端套接字对应的“地方”(包括客户端IP和端口信息等), 第三个参数是“地方”的占地大小。返回值对应客户端套接字标识。

conncet

#include<sys/types.h>
#include<sys/socket.h>
int connect (int sockfd, struct sockaddr * serv_addr, int addrlen);

connect()用来将参数sockfd 的socket 连至参数serv_addr 指定的网络地址。结构sockaddr请参考bind()。参数addrlen为sockaddr的结构长度。
参数一:套接字描述符
参数二:指向数据结构sockaddr的指针,其中包括目的端口和IP地址
参数三:参数二sockaddr的长度,可以通过sizeof(struct sockaddr)获得
成功则返回0,失败返回非0,错误码GetLastError()。

recv

(1)recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,

(2)如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的) [1] 。

recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

int recv( SOCKET s, char FAR *buf, int len, int flags );

不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。

(1)第一个参数指定接收端套接字描述符;

(2)第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;

(3)第三个参数指明buf的长度;

(4)第四个参数一般置0。

sendto

向一指定目的地发送数据。

#include<winsock.h>
int PASCAL FAR sendto( SOCKETs, const char FAR* buf, int len, int flags,const struct sockaddr FAR* to,int tolen);

s:一个标识套接口的描述字。

buf:包含待发送数据的缓冲区。

len:buf缓冲区中数据的长度。

flags:调用方式标志位。

to:(可选)指针,指向目的套接口的地址。

tolen:to所指地址的长度。

getsockopt

获取一个套接字的选项

int getsockopt(int socket, int level, int option_name,void *restrict option_value, socklen_t *restrict option_len);

socket:文件描述符
level:协议层次
SOL_SOCKET 套接字层次
IPPROTO_IP ip层次
IPPROTO_TCP TCP层次
option_name:选项的名称(套接字层次)
SO_BROADCAST 是否允许发送广播信息
SO_REUSEADDR 是否允许重复使用本地地址
SO_SNDBUF 获取发送缓冲区长度
SO_RCVBUF 获取接收缓冲区长度

       SO_RCVTIMEO 获取接收超时时间
       SO_SNDTIMEO 获取发送超时时间
option_value:获取到的选项的值
option_len:value的长度

返回值:
成功:0
失败:-1

setsockopt

获取或者设置与某个套接字关联的选 项。选项可能存在于多层协议中,它们总会出现在最上面的套接字层。当操作套接字选项时,选项位于的层和选项的名称必须给出。为了操作套接字层的选项,应该 将层的值指定为SOL_SOCKET。为了操作其它层的选项,控制选项的合适协议号必须给出。例如,为了表示一个选项由TCP协议解析,层应该设定为协议 号TCP。

#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_toptlen);

sock:将要被设置或者获取选项的套接字。

level:选项所在的协议层。

optname:需要访问的选项名。

optval:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值的缓冲。 optlen:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),现选项的长度。

返回说明:

成功执行时,返回0。失败返回-1,errno被设为以下的某个值

EBADF:sock不是有效的文件描述词

EFAULT:optval指向的内存并非有效的进程空间

EINVAL:在调用setsockopt()时,optlen无效

ENOPROTOOPT:指定的协议层不能识别选项 ENOTSOCK:sock描述的不是套接字

shutdown

shutdown()是指禁止在一个套接口上进行数据的接收与发送。

#include<sys/socket.h>
int shutdown(int sockfd,int how);
#include <winsock.h>或#include <winsock2.h>
int PASCAL FAR shutdown( SOCKET s, int how);

linux下成功则返回0,错误返回-1,错误码errno:EBADF表示sockfd不是一个有效描述符;ENOTCONN表示sockfd未连接;ENOTSOCK表示sockfd是一个描述符而不是socket描述符。

how的方式有三种分别是

SHUT_RD(0):关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。

SHUT_WR(1):关闭sockfd的写功能,此选项将不允许sockfd进行写操作。

SHUT_RDWR(2):关闭sockfd的读写功能。常用结构体

网络编程常用结构体

服务器端开发流程

  • 简单TCPecho服务器
  • 简单UDPecho服务器

客户端开发流程

  • 简单TCPecho客户端
  • 简单UDPecho客户端

流程图

包裹函数

可以直接下载unix网络编程的unp自行编译

多进程并发服务器

实现一个多进程服务器,要求:

  • 允许多个客户端连接
  • 父进程对sigchld信号自定义一个信号处理函数实现输出退出连接的客户段ip以及端口号

多线程并发服务器

多路IO服务器(重要)

select

使用select的开发服务端流程:
1 创建socket, 得到监听文件描述符lfd---socket()
2 设置端口复用-----setsockopt()
3 将lfd和IP  PORT绑定----bind()
4 设置监听---listen()
5 fd_set readfds;  //定义文件描述符集变量
  fd_set tmpfds;
  FD_ZERO(&readfds);  //清空文件描述符集变量
  FD_SET(lfd, &readfds);//将lfd加入到readfds集合中;
  maxfd = lfd;
  while(1)
  {
  	tmpfds = readfds;
  	nready = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
  	if(nready<0)
  	{
  		if(errno==EINTR)//被信号中断
  		{
  			continue;
  		}
  		break;
  	}
  	
  	//有客户端连接请求到来
  	if(FD_ISSET(lfd, &tmpfds))
  	{
  		//接受新的客户端连接请求
  		cfd = accept(lfd, NULL, NULL);
  		
  		//将cfd加入到readfds集合中
  		FD_SET(cfd, &readfds);
  		
  		//修改内核监控的文件描述符的范围
  		if(maxfd<cfd)
  		{
  			maxfd = cfd;
  		}
  		
  		if(--nready==0)
  		{
  			continue;
  		}
  	}
  	
  	
  	//有客户端数据发来
  	for(i=lfd+1; i<=maxfd; i++)
  	{
  		if(FD_ISSET(i, &tmpfds))
  		{
			//read数据
  			n = read(i, buf, sizeof(buf));
  			if(n<=0)
  			{
  				close(i);
  				//将文件描述符i从内核中去除
  				FD_CLR(i, &readfds);
  			}
  			
  			//write应答数据给客户端
  			write(i, buf, n);
  		}
  		
		if(--nready==0)
  		{
  			break;
  		}
  	}
  	
  	close(lfd);
  	
  	return 0;
  }
 

poll

使用poll模型开发服务端流程:
{
1 创建socket, 得到监听文件描述符lfd----socket()
2 设置端口复用----setsockopt()
3 绑定----bind()
4 监听----listen()
5 struct pollfd client[1024];
  client[0].fd = lfd;
  client[0].events = POLLIN;
  
  int maxi = 0;
  for(i=1; i<1024; i++)
  {
  	client[i].fd = -1;
  }
  
  while(1)
  {
  	nready = poll(client, maxi+1, -1);
  	//异常情况
  	if(nready<0)
  	{
  		if(errno==EINTR)  // 被信号中断
  		{
  			continue;
  		}
  		break;
  	}
  	
  	//有客户端连接请求到来
  	if(client[0].revents==POLLIN)
  	{
  		//接受新的客户端连接
  		cfd = accept(lfd, NULL, NULL);
  		
  		//寻找在client数组中可用位置
  		for(i=0; i<1024; i++)
  		{
  			if(client[i].fd==-1)
  			{
  				client[i].fd = cfd;
  				client[i].events = POLLIN;
  				break;
  			}
  		}
  		
  		//客户端连接数达到最大值
  		if(i==1024)
  		{
  			close(cfd);
  			continue;
  		}
  		
  		//修改client数组下标最大值
  		if(maxi<i)
  		{
  			maxi = i;
  		}
  		
  		if(--nready==0)
  		{
  			continue;
  		}
  	}
  	
  	//下面是有客户端发送数据的情况
  	for(i=1; i<=maxi; i++)
  	{
  		sockfd = client[i].fd;
  		//如果client数组中fd为-1, 表示已经不再让你内核监控了, 已经close了
  		if(client[i].fd==-1)
  		{
  			continue;
  		}
  		
  		if(client[i].revents==POLLIN)
  		{
  			//read 数据
  			n = read(sockfd, buf, sizeof(buf));
  			if(n<=0)
  			{
  				close(sockfd);
  				client[i].fd = -1;
  				
  			}
  			else 
  			{
  				//发送数据给客户端
  				write(sockfd, buf, n);
  			} 	
  			
	  		if(--nready==0)
	  		{
	  			break;
	  		}	
  		}  		
  	}  	
  }
 
  close(lfd); 
} 
  

epoll(重要)

使用epoll模型开发服务器流程:
{
1 创建socket, 得到监听文件描述符lfd----socket()
2 设置端口复用----setsockopt()
3 绑定----bind()
4 监听----listen()
5 创建一棵epoll树
  int epfd = epoll_create();
	
  //将监听文件描述符上树
  struct epoll_event ev;
  ev.evetns = EPOLLIN;  //可读事件
  ev.data.fd = lfd;
  epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
  
  struct epoll_event events[1024];
  while(1)
  {
  	nready = epoll_wait(epfd, events, 1024, -1);
  	if(nready<0)
  	{
  		if(errno==EINTR)//被信号中断
  		{
  			continue;
  		}
  		break;
  	}
  	
  	for(i=0; i<nready; i++)
  	{
  		sockfd = events[i].data.fd;
  		//有客户端连接请求到来  	
  		if(sockfd==lfd)
  		{
  			cfd = accept(lfd, NULL, NULL);
  			
  			//将cfd对应的读事件上epoll树
  			ev.data.fd = cfd;
  			ev.evetns = EPOLLIN; 
  			epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
  			continue;
  		}
  		
  		//有客户端发送数据过来
  		n = Read(sockfd, buf, sizeof(buf));
  		if(n<=0)
  		{
  			close(sockfd);
  			//将sockfd对应的事件节点从epoll树上删除
  			epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
  			perror("read error or client closed");
  			continue;
  		}
  		else 
  		{
  			write(sockfd, buf, n);
  		}
  	} 
  }
  
  Close(epfd);
  close(lfd);
  
  return 0;
}

epoll的两种工作模式

  • ET
  • LT

epoll反应堆

epoll反应堆的核心思想是: 在调用epoll_ctl函数的时候, 将events上树的时候,利用epoll_data_t的ptr成员, 将一个文件描述符,事件和回调函数封装成一个结构体, 然后让ptr指向这个结构体, 然后调用epoll_wait函数返回的时候, 可以得到具体的events, 然后获得events结构体中的events.data.ptr指针, ptr指针指向的结构体中有回调函数, 最终可以调用这个回调函数.

练习:

编写一个epool模型的echo服务器

posted on 2021-08-01 23:49  diakla  阅读(141)  评论(0)    收藏  举报