根本数据结构和函数
示意IPv4地址的构造体:
struct sockaddr_in{ sa_family_t sin_family; //示意地址族 uint16_t sin_port; //示意16位的端口号(包含TCP和UDP) struct in_addr sin_addr; //32位的ip地址 char sin_zer[8] ; //不应用 };对于in_addr:struct in_addr{ In_addr_t s_addr;//32位IP地址}
对于下面定义的成员变量类型,大部分都是在POSIX外面定义好了的,属于跨平台可移植的根本数据类型的别名。
对于sockaddr_in成员变量的剖析:
sin_family: 对于每种协定实用于不同的地址族。典型的有:
- AF_INET : IPv4协定应用的地址族
- AF_INET6 : IPv6协定应用的地址族
- AF_LOCAL : 本地通信
- sin_port: 以网络字节序列保留端口号
- sin_zero: 字节填充符,无非凡的含意。
对于socket过程须要绑定socket地址。对应的函数是bind
具体用法:
bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr));
这里须要留神的是struct sockaddr*这个数据结构。在原先老版本的unix网络通信中并没有sockaddr_in这个数据结构,只有sockaddr
struct sockaddr { sa_family_t sin_family; char sa_data[14]; //地址信息}
留神这里的sa_data数组蕴含了sockaddr_in数据结构中前面的所有信息,包含端口号、IP地址等等,这样的结果是编程起来十分不不便。所以前面把这个数据结构转换为了sockaddr_in,但同时也保障了字节序列和这个数据结构统一。因而这里即使应用了强制类型转换也不会影响数据的含意。
网络字节序列
因为不同的机器CPU采纳的存储策略也不同。有些采纳大端法,有些采纳小端法。为了放弃在不同机器之间进行网络通信数据格式的对立,网络序列对立采纳大端法。所以在小端法的机器上发送数据时,首先要转换为大端法。对应的零碎API:
unsigned short htons(unsigned short);//htons:host to network , short numberunsigned long htonl(unsigned long);//host to net , long number unsigned short ntohs(unsigned short);//net to host, short number…………
网络地址初始化
对于IP地址的示意,因为咱们大部分时候是以点分十进制表示法来了解的。然而计算机保留的是二进制序列,所以在初始化真正的IP地址时,咱们须要可能在点分十进制和二进制之间相互转换的API
#include <arpa/inet.h>in_addr_t inet_addr(const char* string);//胜利时返回32位的大端法整数值,失败返回INADDR_NONEint inet_aton(const char *string, struct in_addr * addr);//与上述的API不同点在于,将返回的IP地址间接保留到指针外部//失败了返回0,胜利了返回1char* inet_ntoa(struct in_addr addr);//相同的性能
联合上述的所有内容,个别状况下unix的网络链接初始化的代码如下:
struct sockaddr_in addr;//新建一个网络地址信息对象char * serv_ip = "211.217.168.13"; //输出已知的点分十进制IP地址char * serv_port = "9190" ; //端口号memest(&addr, 0, sizeof(addr));//初始化addr.sin_family = AF_INET;//IPv4的协定地址族addr.sin_addr.s_addr = inet_addr(serv_ip); //初始化字符IPaddr.sin_port = htons(atoi(serv_port)); //初始化字符port。也能够是数字
对于服务端的IP地址,能够应用一个随机初始化变量:INADDR_ANY。个别服务器无限思考这种模式。
最初把初始化信息调配给套接字:
#include <sys/socket.h>int bind(int sockfd, struct sockaddr* myaddr,socklen_t addrlen);
实现基于TCP的服务端
TCP 服务端默认的函数调用程序
socket(); //创立套接字|bind();//分配套接字地址|listen();//监听客户端的申请|accept();//容许连贯|read()/write();//数据交换|close();//敞开连贯
期待连贯申请状态
在服务期端的体现为listen函数
#include <sys/socket.h>int listen(int sock //the socket in server , int backlog);//the size of waiting queue
只有当服务器端处于监听状态能力承受客户端的connect申请,否则会报错。
受理客户端的连贯申请
之后要对处于监听队列的客户端申请作出受理。须要应用accept函数
#include <sys/socket.h>int accept(int sock,// the socket in server struct sockaddr * client_addr, //客户端的socket地址详细信息 socketlen_t * addrlen); //下面的变量的长度
hello_server服务端实例
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#include<sys/socket.h>void error_handling(char *message);int main(int argc,char *argv[]){ int serv_sock; int clnt_sock; struct sockaddr_in serv_addr; struct sockaddr_in clnt_addr; socklen_t clnt_addr_size; char msg[] = "Hello World!"; if(argc != 2){ printf("Usage: %s <port>\n",argv[0]); exit(1); } serv_sock = socket(PF_INET,SOCK_STREAM,0);//create a TCP socket if(serv_sock == -1){ error_handling("socket() error"); } memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(atoi(argv[1])); if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1){ error_handling("bind() error"); } if(listen(serv_sock,5) == -1){ error_handling("listen() error"); } clnt_addr_size = sizeof(clnt_addr); clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size); if(clnt_sock == -1){ error_handling("accept() error"); } write(clnt_sock,msg,sizeof(msg)); close(clnt_sock); close(serv_sock); return 0;}void error_handing(char* message){ fputs(message, stderr); fputc('\n',stderr); exit(-1);}
实现基于TCP的客户端
客户端默认的函数调用程序
socket();//创立套接字|connect();//申请连贯|read()/write();//替换数据|close();//敞开连贯
申请连贯
#include<sys/socket.h>int connect(int sock,//客户端的套接字文件描述符 struct sockaddr* servaddr,//指标服务器的地址信息变量地址 socklen_t addrlen);
客户端调用这个申请的时候,只有当服务器端承受了连贯或者产生异常中断的时候才会失去返回后果。
hello_client客户端实例
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#include<sys/socket.h>void error_handling(char *message);int main(int argc,char **argv){ int sock; struct sockaddr_in serv_addr; char msg[30]; int str_len; if(argc != 3){ printf("Usage : %s <IP> <port>\n",argv[0]); exit(1); } sock = socket(PF_INET,SOCK_STREAM,0); if(sock == -1){ error_handling("socket() error"); } memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); if(connect(sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr)) == -1){ error_handling("connect() error"); } str_len = read(sock,msg,sizeof(msg)-1); if(str_len == -1){ error_handling("read() error"); } printf("Message from server : %s\n",msg); close(sock); return 0;}void error_handling(char* message){ fputs(message,stderr); fputc('\n',stderr); exit(1);}
TCP套接字的缓冲
server端的输入端就是client端的输出端。同时,每个端口都配有缓冲区,单方进行通行的时候,首先把数据放到缓冲区上,而后再从缓冲区进行IO:read就是把对应的字符流退出到缓冲区,write就是把对应的字符流从缓冲中取出
基于UDP的服务端和客户端
UDP与TCP的最次要区别:
TCP存在流控制,而UDP则不存在流控制
同时在编程的时候,理论不存在服务端和客户端的概念,只有发送端和和接收端。因为TCP连贯中,服务端有很多的socket过程,每一个socket对应一个客户端socket。而UDP连贯中不存在这样的货色,哪怕是服务端也只有一个UDP套接字。所以实际上在UDP连贯中,客户端和服务端是处于对等的状态
罕用API
#include <sys/socket.h>ssize_t sendto(int sock, //用于传输数据的套接字文件描述符 void *buff, //带传输的数据地址 size_t nbytes, //须要传输的字节大小 int flags, //选项参数 struct sockaddr* to, //指标地址的详细信息 socklen_t addrlen); //传递给参数to的地址值构造体变量长度ssize_t recvfrom(int sock, //用于传输数据的套接字文件描述符 void *buff, //带承受数据的地址 size_t nbytes, //须要承受的字节大小 int flags, //选项参数 struct sockaddr* from, //源地址的详细信息 socklen_t addrlen); //传递给参数from的地址值构造体变量长度
IP地址和端口调配
咱们都晓得,
断开连接
以上所有代码的断开连接咱们都是应用了close。然而咱们之前也晓得,一个socket同时领有输出流和输入流,如果间接close,就会把输入输出流同时关掉。思考这样的一个场景,服务器要发送大量的数据给客户端,而客户端给服务端响应的内容只有几个字节。因为客户端收到数据后还有进行一系列的操作,如果只为了等那么点信息而始终放弃socket关上显然是不划算的。所以当咱们把数据发送完之后,能够抉择敞开服务端的输入流,共事放弃输出流关上,这样就节俭了肯定的资源,又可能放弃和客户端的通信。
API
int shutdown(int sock, //套接字的文件描述符 int howto)//如何敞开,能够全关,能够只关输出/输入流
域名零碎编程
利用域名获取IP地址
次要用到的API:
#include<netdb.h>struct hostent* gethostbyname(const char* hostname);struct hostent { char * h_name; //官网域名 char ** h_aliases; //通过多个域名可能拜访同一IP,这里保留对应的别名 int h_addrtype; //IPv4 or IPv6 or else int h_length; //IP地址长度 char** h_addr_list; //对应的IP地址列表 }
前面的IP地址列表的变量类型是char**,但实际上指向的是in_addr构造体变量的地址。所以在取得这个成员属性的时候还要做适当的转换。
inet_ntoa(*(struct in_addr*)web_info->h_addr_list[i])
这样就可能失去字符串模式的主机模式IP地址
利用IP地址获取域名
次要API:
struct hostent* gethostbyaddr(const char* addr, //含有IP信息的in_addr构造体指针 socklen_t len, //IPv4的时候为4,IPv6为16 int family); //传递地址族信息
过程相干API
僵尸过程
失常状况下,当父过程生成子过程的时候,期待接管子过程调用的返回后果。
如果父过程收到了这个后果,那么就能够被动销毁子过程;而如果没有,那么就必须让子过程始终不停的在运行,直到父过程本人的工作全副完结。
但即便子过程调用return或者exit函数传递返回值,这些返回值并不会被父过程接管,它们首先被操作系统接管。之后当父过程调用某些特定的函数后才可能从操作系统中获取这些信息进而销毁子过程
销毁僵尸过程的形式
利用wait函数
#include<sys/wait.h>pid_t wait(int * statloc);//将子过程的返回值放入到statloc指针当中。/*然而返回的内容中还有其余的货色,须要进行宏拆散*/WIFEXITED(status):子过程失常终止时返回trueWEXITSTATUS(status):返回子过程的返回值
利用waitpid函数
pid_t waitpid(pid_t pid, //期待被终止的子过程ID,如果为-1,则能够是任意子过程 int* statloc, //与wait函数含意统一 int options); //若传递WNOHANG,即便没有终止的子过程也不会进入阻塞状态,而是返回0并退出胜利时返回终止子过程的ID或者0,失败返回-1
demo:
#include"common.h"int main(int argc, char **argv){ int status; pid_t pid = fork(); if(pid == 0){ sleep(15); exit(24); } else{ while (!waitpid(pid,&status,WNOHANG)) { sleep(3); printf("the parent process sleep for 3 seconds\n"); } if(WIFEXITED(status)){ printf("the child process finished\n,return value is: %d\n",WEXITSTATUS(status)); } } return 0;}
waitpid相比于下面的wait函数,益处在于:
- 可能针对某一个特定的子过程进行销毁操作,而不是像wait函数一样依据工夫的先后顺序。灵活性更强
- 在options参数外面能够设置为非阻塞模式,在某些场景下会更加适宜
signal信号处理
信号处理是在特定的事件产生之后,操作系统向过程发送音讯,为了响应这个音讯,执行与音讯相干的过程就被称为“解决”或者“信号处理”
signal函数
#include<signal.h>void (*signal (int signo, //信号的类型 void (*function)(int))) //解决信号的函数指针(能够了解为handler) (int);//因为返回的是函数指针,所以须要返回给参数为int的某个函数
第一个参数罕用的值:
- SIGALRM: 曾经通过调用alarm函数注册的工夫,也就是说alarm正式失效,收回alarm信号
- SIGINT:通过Ctrl+C按钮终止
- SIGCHILD:子过程终止
alarm函数:
#include <unistd.h>unsigned int alarm(unsigned int seconds); 返回0或者以秒为单位的间隔SIGALRM信号产生所剩工夫
demo:
#include"common.h"void timeout(int sig){ if(sig == SIGALRM){ puts("time out"); } alarm(2);}void keycontrol(int sig){ if(sig == SIGINT){ puts("Ctrl + C pressed"); }}int main(int argc,char **argv){ int i; signal(SIGINT,&keycontrol); signal(SIGALRM,&timeout); alarm(2); for(i =0 ;i<1000;i++){ puts("wait...."); sleep(109); } return 0;}
sigaction函数
之前所用的signal函数目前应用的比拟少了,大部分时候咱们都会用sigaction这个函数来进行信号处理。sigaction函数的劣势在于它在不同的UNIX操作系统中基本上统一。
API:
#include<signal.h>int sigaction(int signo, //信号的信息 const struct sigaction * act, //对应于第一个参数的信号处理动作 struct sigaction* oldact); //获取之前注册的信号处理指针,若不须要置为0struct sigaction{ void (*sa_handler)(int); //保留信号处理函数的指针值 sigset_t sa_mask; //应用sigemptyset(addr)进行初始化 int sa_flags; //0}
利用信号处理函数敞开僵尸过程
原理: 利用子过程应用零碎调用exit 或者 return时,在操作系统中产生子过程完结信号。这个时候只须要咱们写一个处理器,专门用于解决这种僵尸过程,而后把这个处理器和该信号绑定即可。这样,每当产生一个子过程完结信号的时候,就会主动调用read_childproc函数。
#include"common.h"//handle the zombie processvoid read_childproc(int pid){ int status; //because we randomly kill zombie process,"id" is to get the kiled pid pid_t id = waitpid(-1,&status,WNOHANG); if(WIFEXITED(status)){ printf("Removed proc id: %d\n",id); printf("Child send: %d\n",WEXITSTATUS(status)); }}int main(int argc,char **argv){ pid_t pid; struct sigaction act; act.sa_handler = read_childproc; sigemptyset(&act.sa_mask); act.sa_flags = 0; //whenever OS produce a child process end signal, out program will call the handler function sigaction(SIGCHLD, &act,0); pid = fork(); if(pid == 0){ puts("Hi! I'm chlild process\n"); sleep(10); return 12; } else{ printf("Child proc id: %d \n",pid); pid = fork(); if(pid == 0){ puts("Hi! I'm child process"); sleep(10 ); return 24; } else{ int i ; printf("Chlid proc id: %d\n",pid); for(int i = 0;i<5;i++){ puts("wait.."); sleep(5); } } } return 0;}
多过程服务器
demo:
#include"common.h"int main(int argc,char **argv){ int serv_sock, clnt_sock; struct sockaddr_in serv_adr, clnt_adr; pid_t pid; struct sigaction act; socklen_t adr_sz; int str_len, state; char buf[BUF_SIZE]; if(argc != 2){ printf( "Usage : %s <port> \n",argv[0] ); exit(1); } act.sa_handler = read_childproc; serv_sock = socket(PF_INET,SOCK_STREAM,0); sigemptyset(&act.sa_mask); act.sa_flags = 0; state = sigaction(SIGCHLD,&act,0); memset(&serv_adr,0,sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1){ error_handling("bind() error"); } if(listen(serv_sock,5) == -1){ error_handling("listen() error"); } while (1) { adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock,(struct sockaddr*) &clnt_adr, &adr_sz); if(clnt_sock == -1){ continue; } puts("new client connected...."); pid = fork(); if(pid == 0){ close(serv_sock); while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0) { write(clnt_sock,buf,str_len); } close(clnt_sock); puts("connection lost.."); return 0; } else { close(clnt_sock); } } close(serv_sock); return 0;}
须要留神的是,子过程拷贝父过程的内存,然而文件描述符存在于操作系统中的一张表中,复制的是援用,实际上二者指向的是同一个套接字对应的硬件资源。而套接字必须要等所有指向它的文件描述符全副敞开才会被销毁,因而在子过程中必须每次都开释服务器的套接字文件描述符
利用过程对客户端I/O拆散
之前客户端程序的重复read/write实际上节约了很多的工夫,read操作必须要等到服务器端将对应的数据写入到socket中才可能实现。而实际上咱们能够把IO离开,在write的同时就让read操作进入到忙期待状态,这样当处于低网速状态的时候可能十分显著地晋升效率
#include"common.h"void read_routine(int sock, char* buf);void write_routine(int sock, char* buf);int main(int argc ,char **argv){ int sock; struct sockaddr_in serv_addr; char message[BUF_SIZE]; int str_len,recv_len,//用于统计以后的承受数据长度 recv_cnt;//统计每一次接收数据的长度 if(argc != 3){ printf("Usage : %s <IP> <port>\n",argv[0]); exit(1); } sock = socket(PF_INET,SOCK_STREAM,0); if(sock == -1){ error_handling("socket() error"); } memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); if(connect(sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr)) == -1){ error_handling("connect() error"); } puts("Connected....."); pid_t pid = fork(); if(pid == 0){ write_routine(sock,message); } else{ read_routine(sock,message); } close(sock); return 0;}void read_routine(int sock,char *buf){ while(1){ int str_len = read(sock,buf,BUF_SIZE); if(str_len == 0) return; buf[str_len] = 0; printf("Message from server: %s\n",buf); }}void write_routine(int sock,char *buf){ while(1){ fgets(buf,BUF_SIZE,stdin); if(!strcmp(buf,"q\n") || !strcmp(buf,"Q\n")){ shutdown(sock, SHUT_WR); return; } write(sock,buf,strlen(buf)); }}
I/O复用
背景
后面讲到的多过程服务端模型对于每一个来自客户端的申请,都会创立一个新的过程用来解决服务端的IO操作。而事实上,能够把不同套接字的IO操作集成到一个过程上来对立实现,这就是IO复用
select 函数
#include<sys/select.h>#include<sys/time.h>int select(int maxfd,//最大的监听数量 fd_set* readset, //输出流的监听队列 fd_set* writeset,//输入流…… fd_set* exceptset,//异样流…… const struct timeval * timeout);//超时设置/*fd_set的构造 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | ....fd0 fd1 fd2 fd3 fd4.......设置为1示意对于该文件描符进行监听*/
多线程
线程创立和执行
#include <pthread.h>int pthread_create( pthread_t * restrict thread,//保留线程的ID const pthread_attr_t * restrict attr,//默认为NULL,高阶参数 void * (* start_routine)(void *),//对应的线程事件,传递函数指针 void * restrict arg//传递参数的变量地址 ) //胜利则返回0,失败则返回其余的值
int pthread_join( pthread_t thread, //线程的编号 void ** status //线程对应的状态,也就是事件返回的指针变量的值)
线程同步
案例:
#include "common.h"long long num = 0;void* inc(void* arg){ for(int i = 0;i<5000000;i++){ num++; } return NULL;}void * dec(void *arg){ for(int i = 0;i<5000000;i++){ num--; } return NULL;}int main(int argc,char ** argv){ pthread_t t_id1,t_id2; int thread_para1,thread_para2; void *ret_ptr1,*ret_ptr2; if(pthread_create(&t_id1,NULL,inc,(void*)&thread_para1) != 0){ fprintf(stderr,"error!"); exit(1); } if(pthread_create(&t_id2,NULL,dec,(void*)&thread_para2) != 0){ fprintf(stderr,"error2"); exit(1); } if(pthread_join(t_id1,&ret_ptr1) != 0){ fprintf(stderr,"error!"); exit(1); } if(pthread_join(t_id2,ret_ptr2) != 0){ exit(1); } printf("the value of num is : %lld\n",num);}
以上案例是为了证实线程取数据的异样。最初的运行后果(预期为0):
<img class="lazy" referrerpolicy="no-referrer" data-src="C:\Users\lelouch\AppData\Roaming\Typora\typora-user-images\image-20211027130704529.png" alt="image-20211027130704529" style="zoom:100%;" />
能够看到,每一次运行后果都不雷同。起因是num在运行的时候是放在寄存器上,每一次对num++或者--的时候,在cpu上复制num的值,进行加或者减,而后再笼罩回对应的寄存器上。如果恰好两个++操作同时进行,那么num的值同时被更新了两次,在数值上只被+了一次。所以须要线程同步,也就是说,不能同时有两个线程对num进行操作。
互斥量API(Mutex):
互斥量初始化:
int pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t *attr);int pthread_mutex_destroy(pthread_mutex_t * mutex);
锁操作:
int pthread_mutex_lock(pthread_mutex_t * mutex);int pthread_mutex_unlock(pthread_mutex_t * mutex);
信号量API(Semaphore):
//信号量的销毁以及创立#include<semaphore.h>int sem_init(sem_t * sem, //信号量的根底变量 int pshared, //传递0示意只可能被一个线程享受 unsigned int value);//创立信号量的值int sem_destroy(sem_t * sem);//销毁信号量
信号量的P、V操作
int sem_post(sem_t * sem);//对信号量的值+1int sem_wait(sem_t * sem);//对信号量的值-1
sem_wait就相当于用掉一个临界资源。比方输出缓冲区的大小为1,那么在一开始没有输出的时候,输出缓冲的信号量为1 。当获取输出值之后,输出缓冲被用掉,那么就调用一次sem_wait,把信号量的值置为0,示意不可能有其余的线程再来拜访以后的输出缓冲区;直到某个线程取走了这个输出数字,输出缓冲再次空进去,那么就用sem_post,信号量+1 。对于输入缓冲也是一个情理。
案例:线程A从终端输出,线程B获得这个输出,并把输出的值加到num中。
#include "common.h"static sem_t sem_in,sem_out;static int num,tmp;void *produce(void *arg){ for(int i = 0;i<5;i++){ sem_wait(&sem_in); scanf("%d",&tmp); sem_post(&sem_out); } }void *consume(void *arg){ for(int i = 0;i<5;i++){ sem_wait(&sem_out); num += tmp; tmp = 0; sem_post(&sem_in); }}int main(int argc,char ** argv){ pthread_t t_in,t_out; int pthread_para1 ,pthread_para2 = 0; void **ret_adr; //at first the input buffer size is 1,so we need to //set the sem_in value is 1 sem_init(&sem_in,0,1); sem_init(&sem_out,0,0); pthread_create(&t_in,NULL,produce,(void*)&pthread_para1); pthread_create(&t_out,NULL,consume,(void*) &pthread_para2); pthread_join(t_in,ret_adr); pthread_join(t_out,ret_adr); sem_destroy(&sem_in); sem_destroy(&sem_out); printf("%d\n",num); }