管道
消息队列
共享内存
信号
信号量
socket
1.管道
1.1 匿名管道
匿名管道的特点:
- 管道是一个单向的流通信号,
- 管道是面向字节流的,tcp FILE,fstream,我们读的时候是只有字节的概念,并不是说我想读多少,就读多少
- 仅限于父子特性
- 管道回自带同步机制,管道里面没有数据就不会读到管道里面的废弃数据,原子性写入
- 管道的生命周期是随进程的
管道的四种情况:
管道最多可以存放64kb
- 读端不读或者读的慢,那么写端要等读端读完
- 读端关闭,没人读了,写端就没有了存在的必要,写端直接受到SIGPIPE 的信号,直接终止
- 写端不写或者写的慢,读端就要等写端
- 写端关闭,读端就读完全部pipe内部的数据,然后再读就会读到0,表面文件的结尾!
问题分析:为什么下面的代码中,需保证读端比写端快?
因为管道是面向字节流的,字符串之间没由规矩分隔符,如果读取速度慢于写入速度,可能读端还没有将整个字符串读完,写端又写入了数据,会导致数据混乱。
代码实现
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int fd[2];//0:读 1:写
char* str="hello world";
char readbuf[128];
if(pipe(fd)==-1){
printf("creat error!\n");
}
pid_t pid;
pid=fork();//通过fork()建立子进程
if(pid == -1){
printf("pid creat error!\n");
}else if(pid > 0){ //根据pid返回值的大小来判断是子进程还是父进程
sleep(3);//先让子进程运行
close(fd[0]);//写管道要先把读管道关闭;
int write_n=write(fd[1],str,strlen(str)+1);//+1防止传送字符串出现乱码,一次传送过多的字符会乱码
printf("this is father!\n");
wait();//保证读比写快
}else{
printf("this is child!\n");
close(fd[1]);//打开读管道,必须线关闭写管道。
int read_n=read(fd[0],&readbuf,128);
printf("data is %s\n",readbuf);
}
return 0;
}
1.2有名管道
有名管道是提供路径关联,以FIFO文件的形式存在于内存之中,打开方式就和普通的文件一样。
正因为提供了路径关联,就不用像匿名管道那样只能在有亲缘关系的进程里联系。 一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/O系统调用了,与匿名管道一样, FIFO也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的名称也由此而来:先入先出。
FIFO主要有两种用途:
· shell命令使用FIFO将数据从一条管道传送到另一条管道时,无须创建中间的临时文件。
· 客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程二者之间传递数据
一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道;
一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道。
(图片取自@一米九零小胖子)
代码实现
读端代码
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include<errno.h>
//ssize_t read(int fd, void *buf, size_t count);
//ssize_t read(int fd, void *buf, size_t count,size_t size);//size _t count:每次传多大的数据;size_t size:数据大小
//int open(const char *pathname, int flags);
//int mkfifo(const char *pathname, mode_t mode);
int main()
{
char readbuf[128]={0};//声明并初始化数组
int ret = mkfifo("./fifo",0600);//pathname:fifo文件的路径,0600:可读可写操作
if(ret == -1 && errno == EEXIST)){ //返回值EEXIST表示已经存在fifo文件
printf("creat error!\n");
}
int fd = open("./fifo",O_RDONLY);//打开文件并只读文件
if(fd == -1){
printf("open error!\n");
}
int read_n = read(fd,&readbuf,128);
printf("data is %d\n",readbuf);
close(fd);//每次读完都要关闭fifo
return 0;
}
写端代码
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include <fcntl.h>
#include <unistd.h>
#include<stdlib.h>
//ssize_t write(int fd, const void *buf, size_t count);
int main()
{
char* str = “hello wrold”;
int ret = mkfifo("./fifo",0666);
if(ret == -1 ){
printf("creat error!\n");
}
int fd = open("./fifo",O_WRONLY);//打开文件并只写;
if(fd == -1){
printf("open error!\n");
}
int write_n = write(fd,str,strlen(str)+1);
close(fd);//每次写完都要关闭fifo
return 0;
}
2.消息队列
消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。
一般应用于四个场景:异步处理,应用解耦,流量削锋和消息通讯四个场景。
代码实现
读操作
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//int msgget(key_t key, int msgflg);
// int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
key_t key;
key=ftok(".",1);//通过ftok函数来获取1的id号
struct msgbuf readbuf;
int msgid = msgget(key,IPC_CREAT|0666);//通过msgget来创建一个队列,权限是0666:可读可写
if(msgid == -1){
printf("creat error!\n");
}
msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),key,0);//通过msgrcv这个api来获取消息
printf("data is %s\n",readbuf.mtext);
return 0;
}
~
~
写操作
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<stdio.h>
#include<string.h>
#include<stdio.h>
//int msgget(key_t key, int msgflg);
// int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
key_t key;
key=ftok(".",1);
struct msgbuf sendbuf={key,"hellw world"};
int msgid = msgget(key,IPC_CREAT|0666);
if(msgid == -1){
printf("creat error!\n");
}
msgsnd(msgid,&sendbuf,sizeof(sendbuf.mtext),0);//通过msgsend这个api来发送消息
return 0;
}
~
消息队列的应用场景
1.1异步处理
场景:当用户将信息输入数据库后,需要发注册邮件和注册短信。、
传统方法是:
(1)串行方式:用户将信息输入数据库后,先发送邮件再发送注册短信
假如整个流程每个节点的时间为50ms,那么串行方式的总时间则为150ms
(2)并行方式:用户将信息输入数据库后,发送注册邮件的同时发送注册短信
假如整个流程每个节点时间为50ms,那么并行方式的总时间则为100ms
因为cpu在单位时间内处理的请求数是一定的,假设CPU一秒的吞吐量是1000次,那么串行请求量约为7次,并行的请求量约为10次,所以如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。
引用消息队列后
引入消息队列后,用户响应时间为55ms,即输入信息的50ms,由于发送邮件,发送短信写入消息队列后立刻返回,速度极快约为5ms,整个过程的时间约为55ms,比串行快了3倍左右,比并行快了2倍左右
1.2应用解耦
应用场景:用户下单后,订单系统必须通知库存系统。
(1)传统做法是:订单系统调用库存系统接口。
传统做法的缺点是:1.如果库存系统出现问题,订单系统也将出现问题。
2.订单系统和库存系统存在系统耦合
(2)引入消息队列后:
1.订单成功会,写入消息队列,然后立即返回下单成功
2.库存系统订阅下单消息,库存系统根据下单信息,进行库存操作
3.如果下单时库存系统出现故障也不会影响订单的进行,将订单消息写入消息队列后,订单系统就不再关心后续操作了,实现了订单系统和库存系统的应用解耦。
#耦合
耦合是各模块之间相互连接的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。模块之间存在依赖,改动可能会互相影响,关系越紧密,耦合越强,模块独立性越差。比如模块A直接操作了模块B中数据, 则视为强耦合, 若A只是通过数据与模块B交互, 则视为弱耦合。独立的模块便于扩展,维护,写单元测试,如果模块之间重重依赖,会极大降低开发效率。
#内聚
模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。模块内部的元素,关联性越强,则内聚越高,单一性越强。如果有各种场景、功能需要被引入到当前模块, 为了维护代码质量, 建议拆分为多个模块。
#原则
(1)高内聚:一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则
(2)低耦合:模块之间的依赖关系弱
(3)解耦:解除耦合关系。
模块间有依赖关系必然存在耦合,理论上的绝对零耦合是做不到的,只要降低耦合度即可。
让数据模型,业务逻辑和视图显示三层之间彼此降低耦合,把关联依赖降到最低,而不至于牵一发而动全身。
1.3流量削锋
应用场景 :秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。
- 可以控制活动的人数;
- 可以缓解短时间内高流量压垮应用
作用过程:用户输入请求,服务器接收后,首先写入消息队列,如果请求人数过多,超过消息队列最大长度,则先跳转到错误页面,秒杀业务处理再根据消息队列中的请求消息做出处理。
共享内存
共享内存实际是操作系统在实际物理内存中开辟的一段内存
共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。
共享内存实现进程间通信是进程间通信最快的
当一个进程往该空间写入内容时,另一个进程访问该空间,会读到写入的值,实现了进程间的通信
#页表
关于页表的补充:
进程地址空间里有一个内核区域,它们也会在实际物理内存开辟空间,也会有页表与那块空间形成映射关系,这个页表叫做内核级页表。因为内核只有一个,所以每个进程都相同的。说明进程都共用实际物理内存上的内核空间。
除内核空间以外的空间,与实际物理空间之间的页表,称为用户级页表。每个进程可能不同。
代码实现
写入端
#include<sys/ipc.h>
#include<sys/shm.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//int shmget(key_t key, size_t size, int shmflg);
//void *shmat(int shmid, const void *shmaddr, int shmflg);
// int shmdt(const void *shmaddr);
int main()
{
key_t key;
char *shmaddr=NULL;
key=ftok(".",2);//ftok只是利用参数,再运用一套算法,算出一个唯一的key值返回
int shmid=shmget(key,1024*4,IPC_CREAT|0666);//创建共享内存,0666:可读可写权限
if(shmid == -1){//创建失败返回值为1
printf("creat error!\n");
perror("why");//错误原因函数;
exit(-1);
}
shmaddr=(char *)shmat(shmid,0,0);//第一个0:自动获取空间;第二个0:可读可写模式;shmat:共享内存区对
象映射到调用进程的地址空间
strcpy(shmaddr,"hello world");向shmaddr指向的空间写入字符串
sleep(5);//5s后再解除映射和删除共享(给读数据一点时间)
shmdt(shmaddr);//解除映射:使进程中的映射内存无效化,不可以使用,但是保留空间
shmctl(shmid,0,0);//删除共享内存–删除共享内存,彻底不可用,释放空间
return 0;
}
读端
#include<sys/ipc.h>
#include<sys/shm.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//int shmget(key_t key, size_t size, int shmflg);
//void *shmat(int shmid, const void *shmaddr, int shmflg);
// int shmdt(const void *shmaddr);
int main()
{
key_t key;
char *shmaddr=NULL;
key=ftok(".",2);
int shmid=shmget(key,1024*4,0);//创建共享内存,读和写的区别就是读端为0,写端为IPC_CREAT
if(shmid == -1){//创建失败返回值为1
printf("creat error!\n");
perror("why");//错误原因函数;
exit(-1);
}
shmaddr=(char *)shmat(shmid,0,0);//第一个0:自动获取空间;第二个0:可读可写模式;shmat:共享内存区对
象映射到调用进程的地址空间
printf("data is %s\n",shmaddr);
shmdt(shmaddr);//解除映射:使进程中的映射内存无效化,不可以使用,但是保留空间,读端已经释放空间,写端就不用释放了
return 0;
}
为什么已经有一个key来标识共享内存,还需要一个返回值shmid来标识共享内存?因为key是内核级别的,供内核标识,shmget返回值是用户级别的,供用户使用的。
!!!IPC(进程间通信)资源生命周期不随进程,而是随内核的,不释放会一直占用,除非重启。所以,shmget创建的共享内存要释放掉,不然会内存泄漏
!!!写端和读端的key必须相同
ipcs-m : 查看系统中有哪些共享内存
ipcRm-m shmid :删除共享内存
信号
信号其实就是一个软件中断。
在Linux中可以用 kill-l 来查看型号id和名字
信号的产生
硬件产生
通过终端按键产生
- ctrl + c:SIGINT(2),发送给前台进程,& 进程放到后台运行,fg 把刚刚放到后台的进程,再放到前台来运行
- ctrl + z:SIGTSTP(20),一般不用,除非有特定场景
- ctrl + | :SIGQUIT(3),产生core dump文件
软件产生
通过kill命令发送指令:kill-(信号)pid
注册信号
注册信号分为两种:高级:sigaction、低级:signal
信号的意义
信号的意义不在于杀死信号,而是实现一些异步通讯(捕捉信号,用内核调用用户自定义的函数)的手段。
低级注册信号
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
//typedef void (*sighandler_t)(int);
//sighandler_t signal(int signum, sighandler_t handler);
void handler(int signum)
{
printf("signum is %d\n",signum);
printf("nver quit\n");
}
int main()
{
signal(SIGUSR1,handler);//使用signal函数,捕捉SIGUSR1信号来调用用户自定义函数handler
while(1);//让signal一直循环
return 0;
}
高级注册信号
接收流程:
代码实现
接收端代码
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
//int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact)
/*struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);//函数指针,保存了内核对信号的处理方式
sigset_t sa_mask;//默认为0,阻塞状态
int sa_flags;//SA_SIGINFO,OS在处理信号的时候,调用的就是sa_sigaction函数指针当中,保存的值0,在处理信号的时候,调用sa_handler保存的函数
void (*sa_restorer)(void);
};*/
void handler(int signum, siginfo_t *info, void *contex)
{
if(contex != NULL){
printf("signum is %d\n",signum);
printf("data is %d\n",info->si_int);
printf("data is %s\n",(char *)info->si_value.sival_ptr);
}
}
int main()
{
printf("pid is %d\n",getpid());
struct sigaction act;
act.sa_flags = SA_SIGINFO; //设置该参数表示可以接收额外的参数
act.sa_sigaction = handler;//定义接受信号的处理函数
sigaction(SIGUSR1,&act,NULL);
while(1);
return 0;
}
发送端代码
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
//int sigqueue(pid_t pid, int sig, const union sigval value);
/* union sigval {
int sival_int;
void *sival_ptr;
}; */
int main(int argc , char** argv)
{
signum = atoi(argv[1]);
pid = atoi(argv[2]);
union sigval value;
value.sival_int =100;
value.sival_ptr ="hello world";
sigqueue(pid,signum,value);
return 0;
}
信号捕捉
信号的捕捉是指信号的处理动作是用户自定义函数,信号递达时就是调用这个函数。
内核态返回用户态会调用do_signal函数,两种情况:
- 无信号:sys_return函数,返回用户态
- 有信号:先处理信号,信号返回,再调用do_signal函数
例: 程序注册了SIGQUIT信号的处理函数sighandler,当前正在执行main函数,这时发生中断或异常切换到内核态,在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达,内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数, sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
信号量
信号量:有时被称为信号灯,是操作系统用来解决并发中的互斥和同步问题的一种方法。进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量;
信号量的特性:
信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
-
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
-
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
-
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
-
支持信号量组
临界区
在程序中有时会出现一段特殊的代码,同一时间只允许一个进程执行该部分代码,这部分区域被称为“临界区”。在多进程并发执行时,当一个进程进入临界区,因为某种原因被挂起时,其他进程也有可能进入该区域-----------------解决办法是:信号量
P操作和V操作
信号量是一种特殊的变量,就好像一把钥匙,第一个进程拿走钥匙打开锁后,其他进程就必须在门口等待,当地一个进程归还钥匙后第二个进程才能拿钥匙去开锁
P操作:申请资源(拿钥匙)
1.如果信号量的值>0,则把信号量-1(可等同于拿走了钥匙,钥匙数-1)
2.如果信号量的值=0,则挂起该进程
v操作:释放资源(还钥匙)
1.如果有进程因为信号量被挂起,则恢复当前进程运行
2.如果没有进程被挂起,则把信号量+1(可等同于放回了钥匙,钥匙数+1)
信号量代码实现
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include<stdio.h>
// int semget(key_t key, int nsems, int semflg); //nsems:信号量集中信号量的个数;semflg:IPC_CREAT|IPC_EXCL:不存在创建,存在出错返回;IPC_CREAT:不存在创建,存在返回
// int semctl(int semid, int semnum, int cmd, ...);cmd:采取的操作
union semun {
int val; /* Value for SETVAL */ //只用这一个
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
void p(int semid)
{
struct sembuf spos;
spos.sem_num=0;//钥匙序号,第一把为0;
spos.sem_op=-1;//取走钥匙-1
spos.sem_flg=SEM_UNDO;
semop(semid,&spos,1);
printf("key out\n");
}
void v(int semid)
{
struct sembuf spos;
spos.sem_num=0;//钥匙序号,第一把为0;
spos.sem_op=+1;//还钥匙+1
spos.sem_flg=SEM_UNDO;
semop(semid,&spos,1);
printf("key back\n");
}
int main()
{
key_t key;//(同消息队列和共享内存)信号量集的名字
int semid = semget(key,1,IPC_CREAT|0666);//当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1
union semun initstem;
initstem.val = 0;//没有钥匙
semctl(semid,0,SETVAL,initstem);//SETVAL:设置信号量集中信号量的计数值(初始化信号量)为initstem; initstem:有几个信号量的钥匙
pid_t pid;
pid=fork();
if(pid > 0){
p(semid);//semid:semget返回的信号量集的标识符;
printf("this is father\n");
v(semid);
}else if(pid == 0){
v(semid);
printf("this is child\n");
}
return 0;
}
cmd命令
挂起
挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪。
阻塞与挂起的区别
1.是否释放CPU:阻塞(pend)就是任务释放CPU,其他任务可以运行,一般在等待某种资源或信号量的时候出现。挂起(suspend)不释放CPU,如果任务优先级高就永远轮不到其他任务运行。一般挂起用于程序调试中的条件中断,当出现某个条件的情况下挂起,然后进行单步调试。
2.是否主动:显然阻塞是一种被动行为,其发生在磁盘,网络IO,wait,lock等要等待某种事件的发生的操作之后。因为拿不到IO资源,所以阻塞时会放弃 CPU的占用。而挂起是主动的,因为挂起后还要受到CPU的监督(等待着激活),所以挂起不释放CPU,比如sleep函数,站着CPU不使用。
3.与调度器是否相关:任务调度是操作系统来实现的,任务调度时,直接忽略挂起状态的任务,但是会顾及处于pend下的任务,当pend下的任务等待的资源就绪后,就可以转为ready了。ready只需要等待CPU时间,当然,任务调度也占用开销,但是不大,可以忽略。可以这样理解,只要是挂起状态,操作系统就不在管理这个任务了。
阻塞挂起状态(Blocked, suspend):进程在外存并等待某事件的出现
就绪挂起状态(Ready, suspend):进程在外存,但只要进入内存,即可运行
同步和互斥
同步:合作进程间的直接制约关系
进程同步:进程间完成任务时直接发生相互作用的关系
互斥:申请临界资源进程间的间接制约关系
进程互斥:进程间互斥使用临界资源
生产者和消费者
(122条消息) sigemptyset、sigaddset、sigprocmask的用法 信号未决,信号阻塞 信号的捕捉_猿侠令狐冲的博客-CSDN博客
socket
1.TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2. TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3. TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的 UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4. 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5. TCP首部开销20字节;UDP的首部开销小,只有8个字节
6. TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
端口号作用
一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP 地址与网络服务的关系是一对多的关系。 实际上是通过“IP地址+端口号”来区 分不同的服务的。 端口提供了一种访问通道, 服务器一般都是通过知名端口号来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69
字节序
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。,如果你的主机不是采用大端字节序就转为大端字节序在发送数据流时就回转换成大端字节序,如果你是大端字节序就不进行转换,IP地址也一样都要转换成大端字节序(IP地址是用4个字节来保存所以要用htonl,端口用htons)
socket服务器 API
建立服务器的流程
int socket(int domain, int type, int protocol);//(指定“语言”[连接协议])
//成功返回非负描述符,失败返回-1
domain 网络层相关协议 type 信息传送方式 protocol 运输层相关协议,如果前两个都确定了,系统就可以自动选择协议了。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//绑定实际地址
//返回值:成功则为0,失败为-1
一般使用的是struct sockaddr_in
1. sin_family 一般设为AF_INET
2. sin_pory :设置端口号 -------------一定记得转换
(例:如果想把端口号设为8888,则:htons(888))
#include <netinet/in.h> uint16_t htons(uint16_t host16bitvalue); //返回网络字节序的值 uint32_t htonl(uint32_t host32bitvalue); //返回网络字节序的值uint16_t ntohs(uint16_t net16bitvalue); //返回主机字节序的值 uint32_t ntohl(uint32_t net32bitvalue); //返回主机字节序的值 h代表host,n代表net,s代表short(两个字节),l代表long(4个字节),通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR_ANY,INADDR_ANY指定地址让操作系统自己获取
3.sin_addr 是ip地址的结构体 应该用函数配置一下:inet_aton("192.168.43.175(由ifconfig查询)",&s_addr.sin_addr))//函数的意思是将ip地址转换成网络能够识别的格式
int listen(int sockfd, int backlog);//例:list(s_fd,10);
连接
struct sockaddr c_addr;//一定要重新声明一个结构体变量,后面的读写函数都用c_fd作为标志
int c_fd = accept(int sockfd,struct sockaddr *addr,socklen_t *arrlen)
//例:int c_fd=accept(s_fd,(strucy sockaddr *)&c_addr.sin_addr,&arrlen);
服务器代码实现
#include <sys/types.h>
#include <sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
int main(int argc ,char **argv)
{
//socket :int socket(int domain, int type, int protocol);
int c_fd;
int arrlen;
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
char readbuf[128]={0};
int mark = 0;
int read_n=0;
int write_n=0;
char ptr[128] ={0};
int s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd == -1){
perror("why");
exit(-1);
}
//bind: int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr_in s_addr;
memset(&s_addr,0,sizeof(struct sockaddr_in));
s_addr.sin_family =AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//listen
listen(s_fd,10);
//accept : int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
arrlen = sizeof(struct sockaddr_in);
while(1){
c_fd=accept(s_fd,(struct sockaddr *)&c_addr,&arrlen);
if(c_fd == -1){
perror("why");
exit(-1);
}
mark++;
printf("%s connect success!\n",inet_ntoa(s_addr.sin_addr));
if(fork()==0){
if(fork() == 0){
while(1){
sprintf(ptr,"%d connect\n",mark);
write_n = write(c_fd,ptr,strlen(ptr));
sleep(3);
}
}
while(1){
memset(&readbuf,0,sizeof(readbuf));
read_n = read(c_fd,&readbuf,128);
if(read_n == -1){
perror("connect:");
}else{
printf("he: %s\n",readbuf);
}
}
break;
}
}
return 0;
}
客户端代码实现
#include <sys/types.h>
#include <sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
int main(int argc,char **argv)
{
//socket :int socket(int domain, int type, int protocol);
int read_n=0;
int write_n=0;
char readbuf[128]={0};
char ptr[128] = {0};
int c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd == -1){
perror("why");
exit(-1);
}
//connect : int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
int addrlen =sizeof(struct sockaddr_in);
connect(c_fd,(struct sockaddr *)&c_addr,addrlen);
while(1){
if(fork() == 0){
while(1){
memset(ptr,0,sizeof(ptr));
printf("my:\n");
gets(ptr);
write_n = write(c_fd,ptr,strlen(ptr));
}
}
while(1){
read_n = read(c_fd,&readbuf,128);
printf("he :%s\n",readbuf);
}
}
return 0;
}
while(1){ c_fd=accept(s_fd,(struct sockaddr *)&c_addr,&arrlen); if(c_fd == -1){ perror("why"); exit(-1); } mark++; printf("%s connect success!\n",inet_ntoa(s_addr.sin_addr)); if(fork()==0){ if(fork() == 0){ while(1){ sprintf(ptr,"%d connect\n",mark); write_n = write(c_fd,ptr,strlen(ptr)); sleep(3); } } while(1){ memset(&readbuf,0,sizeof(readbuf)); read_n = read(c_fd,&readbuf,128); if(read_n == -1){ perror("connect:"); }else{ printf("he: %s\n",readbuf); } } break; } }
我们单独分析服务器这段代码:通过fork()函数建立子进程,当fork() == 0表示已经建立并进入了子进程,那么父进程在原地等待客户端的请求,所以父进程while(1)等待,切read函数在没有数据到来时保持堵塞状态,而子进程则用于处理来自客户端的请求。我们声明了一个mark作为标志位,acdept一次mark自加1,当有一个客户端连接时,反馈给客户端是第几个连接(用sprintf来实现自动回复)