目录在左侧!!!

本文主要介绍了线程编程、网络编程和TCP/IP协议的核心知识。线程部分详细讲解了线程创建、同步控制(互斥锁、读写锁、条件变量)和线程通信机制;网络编程部分涵盖了TCP/UDP套接字编程模型,包括socket、bind、listen等关键函数;最后解析了TCP/IP协议栈的分层结构和TCP三次握手/四次挥手流程。文章通过代码示例演示了多线程服务器、I/O多路复用等核心技术的实现方法,并比较了阻塞/非阻塞I/O模型的差异。这些内容为开发高性能网络应用提供了理论基础和实践指导。

一、线程:

概念

  • 1.创建线程之后,地址空间没有变化
  • 2.进程退出变成了线程--主线程
  • 3.创建出的子线程和主线程共用地址空间
  • 4.主线程和子线程有各自独立的PCB
  • 5.子线程的PCB是从主线程拷贝来的

主线程和子线程除了<栈>不共享,其余均共享(text、bss、date、堆、动态加载区、环境变量、命令行参数)

PS:如果一共五个线程,那么栈区会被平均分成五块。

在linux下:线程就是进程-轻量级进程;但对于内核来说,线程就是进程。

多进程和多线程的区别

  • 多进程:始终共享的资源:代码、文件描述符、内存映射区-mmap
  • 多线程:始终共享的资源:堆、全局变量、节省资源

安装线程man page,命令:sudo apt-get install manpages-posix-dev

查看指定线程的LWP号:
线程号和线程ID是有区别的:线程号是给内核看的

查看方式:

  • 1.找到程序的进程ID
  • 2.ps -Lf pid

1.pthread_create函数

作用:创建线程

函数原型:

int pthread_create (pthread_t  *thread,
                    const pthread_attr_t *attr ,//线程属性,NULL
                    void *(*start_routine )(void *),//线程处理函数
                    void *arg);//线程处理函数

返回值:

  •              成功:0
  •              错误:错误号   //perror不能使用该函数打印错误信息,可以使用strerror来打印

参数说明:

pthread:传出参数,线程创建成功之后,会被设置一个合适的值

attr:默认传NULL

start_routine:子线程的处理函数

arg:回调函数的参数

PS:

  • 主线程先退出,子线程会被强制结束
  • 验证线程直接共享全局变量

pthread_create函数代码实战:

实现我们的线程创建功能,用test.c文件来操作,内部程序如下:

 
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
 
void* myfun(void* argv)
{
        printf("child pathread id is %ld\n",pthread_self());
        return 0;
}
 
int main()
{
        pthread_t pthid;
 
        pthread_create(&pthid,NULL,myfun,NULL);
        printf("parent pathread id is %ld\n",pthread_self());
 
        for(int i = 0;i < 5;i++)
        {
                printf("i = %d\n",i);
        }
 
        sleep(2);
 
        return 0;
}

要注意下:gcc编译时需要链接pthread库!!,用-lpthread 来解决

可以看到“要主线程睡眠才能执行子线程,否则父线程退出,子线程自动结束

pthread_create函数代码实战2:

这次代码出现了突发情况,同样用test.c文件来操作,内部程序如下:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
 
void* myfunc(void *arg)
{
        printf("child pthread id is :%ld\n",pthread_self());
        return 0;
}
 
int main()
{
        pthread_t pthid;
        int ret;
 
        ret = pthread_create(&pthid,NULL,myfunc,NULL);
 
        if(ret != 0)
        {
                printf("error number is :%d\n",ret);
                printf("%s\n",strerror(ret));
        }
 
        printf("parent pthread id is :%ld\n",pthread_self());
 
        for(int i = 0;i < 5;i++)
        {
                printf("i = %d\n",i);
        }
 
        sleep(2);
 
        return 0;
}

但这次出现的结果图却不一样。

可以通过结果图看到,两次打印“子线程”的时间不一样,这是为什么呢?

答:第一次运行时,主线程打印完parent pthread id后,刚要进入循环,就被操作系统的调度器 “切走了” CPU,子线程先拿到了时间片,所以先打印了child pthread id

这两种顺序,甚至更多种混合顺序,都是Linux 线程调度的正常表现

  • 操作系统的调度器会根据系统负载、CPU 状态、线程优先级等因素,动态分配 CPU 时间片,程序本身无法预测和控制。
  • 代码里没有任何 “同步机制”,两个线程的执行顺序完全是随机的,没有所谓的 “标准顺序”。

2.pthread_exit函数

作用:单个线程退出

函数原型:

void pthread_exit(void *retval);

参数说明:

retval指针:必须指向全局,堆

pthread_exit函数代码实战:

实现我们的线程退出功能,用test.c文件来操作,内部程序如下:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
 
void* myreturn()
{
    return 0;
}
 
void* myfunc(void* argv)
{
        printf("child pthid is :%ld\n",pthread_self());
        for(int i = 0;i < 5;i++)
        {
                printf("child pthread i is:%d\n",i);
                if(i == 2)
                {
                        pthread_exit(NULL);
                        //return 0;
                        //return NULL;
                        //myreturn();//从函数入口退出
                }
        }
    return 0;
}
 
int main()
{
        pthread_t pthid;
        int ret;
 
        ret = pthread_create(&pthid,NULL,myfunc,NULL);
 
        if(ret != 0)
        {
                printf("error number is :%d\n",ret);
                printf("%s\n",strerror(ret));
        }
        printf("parent ptrhid is :%ld\n",pthread_self());
 
        for(int i = 0;i < 5;i++)
        {
                printf("parent pthread i is:%d\n",i);
        }
 
        pthread_exit(NULL);
 
        printf("after exit\n");
        sleep(2);
 
        return 0;
}

看程序运行之后的图,可以看到必须得等父进程退出后才会进入子进程pthread_exit(NULL)只结束当前线程,不影响其他线程。

exit(0)函数代码实战:

实现我们的所有线程退出功能,用test.c文件来操作,内部程序如下:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
 
void* myfunc(void* argv)
{
        printf("child pthid is :%ld\n",pthread_self());
        for(int i = 0;i < 5;i++)
        {
                printf("child pthread i is:%d\n",i);
                if(i == 2)
                {
                        exit(0);
                }
        }
}
 
int main()
{
        pthread_t pthid;
        int ret;
 
        ret = pthread_create(&pthid,NULL,myfunc,NULL);
 
        if(ret != 0)
        {
                printf("error number is :%d\n",ret);
                printf("%s\n",strerror(ret));
        }
        printf("parent ptrhid is :%ld\n",pthread_self());
 
        int i = 0;
        while(1)
        {
                i++;
                printf("parent pthread i is:%d\n",i);
        }
 
        sleep(2);
 
        return 0;
}

看程序运行之后的图,可以看到exit(0)之后,所有线程都结束了

  • 但为什么还会有一部分主循环的打印再“子线程打印”后出现?
  • 因为这跟操作系统的调度有关了,如果线程没有结束,那么主线程应该还在执行“无限循环”。

3.pthread_join函数

 作用:阻塞等待线程退出,获取线程退出状态。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。

函数原型:

int pthread_join(pthread_t pthread,void **retval);

返回值:

  • 成功,返回0。
  • 失败,返回的则是错误号。

参数说明:

pthread:要回收的子线程的ID

retval:读取线程退出的携带信息

传出参数:

  •                    void *ptr;
  •                    pthread_join(pthid,&ptr);
  •                    指向的内存和pthread_exit参数指向地址一致

pthread_join函数代码实战:

实现我们的线程回收功能,用test.c文件来操作,内部程序如下:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
 
int num = 10;
 
void* myfunc(void* argv)
{
        printf("child pthid is :%ld\n",pthread_self());
        for(int i = 0;i < 5;i++)
        {
                printf("child pthread i is:%d\n",i);
                if(i == 2)
                {
                        pthread_exit(&num);
                }
        }
}
 
int main()
{
        pthread_t pthid;
        int ret;
 
        ret = pthread_create(&pthid,NULL,myfunc,NULL);
 
        if(ret != 0)
        {
                printf("error number is :%d\n",ret);
                printf("%s\n",strerror(ret));
        }
        printf("parent pthid is :%ld\n",pthread_self());
 
        void* ptr = NULL;
        pthread_join(pthid,&ptr);
        printf("num is :%d\n",*(int *)ptr);
 
        int i = 0;
        while(i < 10)
        {
                i++;
                printf("parent pthread i is:%d\n",i);
        }
 
        pthread_exit(NULL);
 
        sleep(2);
 
        return 0;
}

看下面程序运行图,可以看出我们的pthread_join函数阻塞了我们的主线程,等待子线程结束,然后回收子线程资源。再继续执行我们的主线程:

4.pthread_detach函数

 作用:线程分离。注意这个函数应该当线程创建时,在线程属性中设置

函数原型:

int pthread_detach(pthread_t thread);

返回值:

  • 成功,返回0
  • 失败,返回错误码

参数说明:

thread:待分离的线程ID

pthread_detach函数与pthread_join函数的区别:

  • pthread_join()即是子线程合入主线程,主线程阻塞等待子线程结束,然后回收子线程资源。
  • pthread_detach()即主线程与子线程分离,子线程结束后,资源自动回收。子线程会自动回收自己的PCB 。
特性 pthread_detach pthread_join
资源回收方式 自动 手动
线程状态获取  不可获取返回值 可获取线程返回值
调用时机 线程运行期间 线程结束后
多线程场景适用性 场景适用性多 场景适用性一般

5.pthread_cancel函数

 作用:

函数原型:杀死(取消)线程

int pthread_cancel(pthread_t pthread);

返回值:

  •              成功:0
  •              错误:错误号  

参数说明:

pthread:线程ID

ps:

  • 在要杀死的子线程对应的处理的函数的内部,必须做过一次系统调用(write read printf)
  • int a=2;int b=a+3;单这种语句不可以
  • pthread_testcancel():设置取消点

pthread_cancel函数代码实战:

实现我们的杀死(取消)线程功能,用test.c文件来操作,内部程序如下:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
 
int num = 10;
 
void* myfunc(void *argv)
{
        int i,j,k;
 
        while(1)
        {
                i = 10;
                j = 20;
                k = 30;
                printf("i = %d,j = %d,k = %d\n",i,j,k);
                pthread_testcancel();
        }
        return &num;
}
 
int main()
{
        pthread_t pthid;
        int ret;
 
        ret = pthread_create(&pthid,NULL,myfunc,NULL);
        if(ret != 0)
        {
                printf("error number is %d\n",ret);
                printf("%s\n",strerror(ret));
        }
        printf("parent pthread id is:%ld\n",pthread_self());
 
        sleep(1);
        pthread_cancel(pthid);
 
        int i = 0;
        while(i < 10)
        {
                i++;
                printf("parent pthread i: %d\n",i);
        }
 
        pthread_exit(NULL);
 
        sleep(0);
 
        return 0;
}

看下面程序运行图,可以看出我们的pthread_cancel函数杀死了我们子线程功能。

6.pthread_equal函数

 作用:比较两个线程ID是否相等(预留函数)

函数原型:

int pthread_equal(pthread_t   t1,pthread_t   t2);

返回值:

  • 相等,返回非0值
  • 不等,返回0值

参数说明:

t1,t2:线程ID

7.线程的分离属性

通过属性设置线程的分离,分离线程之后,会自动回收子线程:

1.线程属性类型:pthread_attr_t attr;

2.线程属性操作函数:

(1)对线程属性变量的初始化:

函数原型:

int   pthread_attr_init(pthread_attr_t *attr);

返回值:

  •              成功:0
  •              错误:错误号  

(2)设置线程分离属性:

函数原型:

int  pthread_attr_setdetachstate(pthread_attr_t *attr,int  detachstate);

返回值:

  • 成功,那么pthread_attr_set拆离状态 () 将返回 0。

  • 不成功,pthread_attr_setdetachstate( ) 返回 -1。

参数说明:

attr:线程属性

detachstate:PTHREAD_CREATE_DETACH (分离)
                       PTHREAD_CREATE_JOINABLE(非分离)

(3)释放线程资源函数:

函数原型:

int pthread_attr_destroy(pthread_attr_t *attr);

返回值:

  • 成功, pthread_attr_destroy () 将返回 0。

  • 不成功,pthread_attr_destroy( ) 返回 -1。

设置线程的分离代码实战:

test.c文件来操作,内部程序如下:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
 
void* myfunc(void *argv)
{
        printf("child pthread id:%ld\n",pthread_self());
        return 0;
}
 
int main()
{
        pthread_t pthid;
        int ret;
 
        pthread_attr_t attr;
        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
 
        ret = pthread_create(&pthid,&attr,myfunc,NULL);
        if(ret != 0)
        {
                printf("error number is %d\n",ret);
                printf("%s\n",strerror(ret));
        }
        printf("parent pthread id is:%ld\n",pthread_self());
 
        for(int i = 0;i < 5;i++)
        {
                printf("i = %d\n",i);
        }
 
        sleep(2);
 
        pthread_attr_destroy(&attr);
 
        return 0;
}

看下面程序运行图,可以看出我们的 pthread_attr_setdetachstate函数杀分离线程之后,会自动回收子线程。

8.线程同步

数据混乱:操作了共享资源;CPU调度问题

解决:线程同步

什么是同步?协同步调,按照先后顺序操作执行。

9.互斥量(互斥锁)

互斥锁类型:pthread_mutex mutex;

互斥锁的特点:多个线程访问共享数据的时候是串行的

使用互斥锁缺点:效率低

互斥锁使用的步骤

1.创建互斥锁:pthread_mutex_t mutex

2.初始化:pthread_mutex_init(&mutex,NULL),---mutex =1

3.找到线程共同操作的共享数据:

  • 1.加锁:操作共享资源之前加锁,pthread_mutex_lock(&mutex);//阻塞  -- mutex=0               
  • 2.pthread_mutex_trylock(&mutex);//如果锁上锁直接返回,不阻塞(xxxxxx共享数据操作,临界区,越小越好)
  • 3.解锁:pthread_mutex_unlock(&mutex);//---mutex =1  (阻塞在锁上的线程会被唤醒)

4.销毁:pthread_mutex_destroy(&mutex);

互斥锁相关函数

<1>初始化互斥锁:

(restrict是关键字)

函数原型:

pthread_mutex_init(pthread_mutex_t* restrict mutex,const pthread_mutex attr* restrict attr)

<2>销毁互斥锁:

函数原型:

pthread_mutex_destroy(pthread_mutex_t* mutex);

<3>加锁:

函数原型:

pthread_mutex_lock(pthread_mutex* mutex);


mutex:  没有被锁上,当前线程会将这把锁锁上。
             被锁上了:当前线程阻塞,锁被打开之后,线程解除阻塞

<4>尝试加锁,失败返回,不阻塞:

函数原型:

pthread_mutex_trylock(pthread_mutex_t* mutex);


没有锁上:当前线程会被这把锁加锁

如果锁上了:不会阻塞,返回

  • 返回0:加锁成功。
  • 没锁上:返回错误号
一般代码编写为:
if(pthread_mutex_trylock(&mutex)==0)
{
        //尝试加锁,并且成功了
         //访问共享资源
}
else
{
         //错误处理
         //或者等待,再次尝试加锁
}

<5>解锁:

函数原型:

pthread_mutex_unlock(pthread_mutex_t* mutex);

PS:如果我们想要使用互斥锁同步,那么所有线程都需要加锁

不使用互斥锁代码实战

test.c文件来操作,内部程序如下:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
 
int number = 0;
#define MAX 10000
 
void* fun1(void* arg)
{
        for(int i = 0;i < MAX;i++)
        {
                int ret = number;
                ret++;
                number = ret;
                printf("fun1:  %ld,%d\n",pthread_self(),number);
 
                usleep(10);
        }
        return NULL;
}
 
void* fun2(void* arg)
{
        for(int i = 0;i < MAX;i++)
        {
                int ret = number;
                ret++;
                number = ret;
                printf("fun2:%ld,%d\n",pthread_self(),number);
 
                usleep(10);
        }
        return NULL;
}
 
int main()
{
        pthread_t pth1,pth2;
 
        pthread_create(&pth1,NULL,fun1,NULL);
        pthread_create(&pth2,NULL,fun2,NULL);
 
        pthread_join(pth1,NULL);
        pthread_join(pth2,NULL);
 
        return 0;
}

看下面程序运行图,可以看到线程运行期间出现抢占,很混乱,甚至可能不会输出到最后的20000,这就是没加互斥锁。

使用互斥锁代码实战

test.c文件来操作,内部程序如下:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
 
#define MAX 10000
int number = 0;
 
pthread_mutex_t mutex;
 
void* fun1(void* arg)
{
        for(int i = 0;i < MAX;i++)
        {
                pthread_mutex_lock(&mutex);
 
                int ret = number;
                ret++;
                number = ret;
                printf("fun1:  %ld,%d\n",pthread_self(),number);
 
                pthread_mutex_unlock(&mutex);
 
                usleep(10);
        }
        return NULL;
}
 
void* fun2(void* arg)
{
        for(int i = 0;i < MAX;i++)
        {
                pthread_mutex_lock(&mutex);
 
                int ret = number;
                ret++;
                number = ret;
                printf("fun2:%ld,%d\n",pthread_self(),number);
 
                pthread_mutex_unlock(&mutex);
 
                usleep(10);
        }
        return NULL;
}
 
int main()
{
        pthread_mutex_init(&mutex,NULL);
 
        pthread_t pth1,pth2;
 
        pthread_create(&pth1,NULL,fun1,NULL);
        pthread_create(&pth2,NULL,fun2,NULL);
 
        pthread_join(pth1,NULL);
        pthread_join(pth2,NULL);
 
        pthread_mutex_destroy(&mutex);
 
        return 0;
}

看下面程序运行图,可以看到互斥锁(mutex)保护共享资源,防止数据竞争,并且安全输出到我们要的值20000.

10.原子操作

CPU处理一个指令,进程/线程在处理完这个指令之前是不会失去CPU的,就像原子被认为是不可分割颗粒一样

PS:加锁的目的是防止数据混乱

11.死锁

造成死锁的原因:

(1)自己锁自己:就是前置下连续锁自己两次,因此要记得操作做完之后,一定要解锁。

(2)线程1对共享资源A加锁成功---A锁
         线程2对共享资源B加锁成功---B锁
   所以,当这时,线程1访问共享资源B,对B锁加锁--线程1阻塞在B锁上
                             线程2访问共享资源A,对A锁加锁--线程2阻塞在A锁上

解决(2)的方法

  • <1>让线程按照一定的顺序去访问共享资源
  • <2>在访问其他锁的时候,需要先将自己的锁解开
  • <3>用trylock

12.读写锁

<1>读写锁是一把锁,pthread_rwlock_t lock

<2>读写锁的类型:读锁:对内存做读操作
                                写锁:对内存做写操作

<3>读写锁的状态

<4>读写锁的特性:线程A加读锁成功,又来了三个线程,做读操作,可以加锁成功(读共享-并行处理)
                                线程A加写锁成功,又来了三个线程,做读操作,三个线程阻塞(写独占)
                                线程A加读锁成功,又来了B线程加写锁阻塞,又来了C线程加读锁阻塞(读写不可以同时进行,写的优先级高)

<5>读写锁场景练习:线程A加写锁成功,线程B请求读锁(线程B阻塞)
                                   线程A持有读锁,线程B请求写锁(线程B阻塞)
                                   线程A拥有读写,线程B请求读锁(线程B加锁)
                                   线程A持有读锁,然后线程B请求写锁,然后线程C请求读锁(1.线程B阻塞,线程C阻塞;2.线程B加锁,线程C阻塞;3.线程C加锁)
                                   线程A持有写锁,然后线程B请求读锁,然后线程C请求写锁(1.线程B阻塞,线程C阻塞;2.线程C加锁,线程B阻塞;3.线程B加锁)

<6>读写锁的适用场景:互斥锁:读写串行
                                       读写锁:读:并行
                                                      写:串行
                                      程序中的读操作>写操作的时候

<7>主要操作函数:

12.1 初始化读写锁

函数原型:

pthread_rwlock_init(pthread_rwlock_t* restrict rwlock ,const pthread_rwlockattr_t* restrict attr);

12.2 销毁读写锁

函数原型:

pthread_rwlock_destroy(pthread_rwlock_t* rwlock);

12.3 加读锁

函数原型:

pthread_rwlock_rdlock(pthread_rwlock_t* rdlock);

PS:阻塞:之前对这把锁加的是写锁的操作

12.4 尝试加读锁

函数原型:

pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);

加锁:

  •                  成功:返回0
  •                  失败:返回错误号

12.5 加写锁

函数原型:

pthread_rwlock_wrlock(pthread_rwlock_t* wrlock);

PS

  •          阻塞:上一次加写锁还没解锁
  •          阻塞:上一次加读锁还没解锁

12.6 尝试加写锁

函数原型:

pthread_rwlock_trywrlock(pthread_rwlock_t* wrlock);

12.7 解锁

函数原型:

pthread_rwlock_unlock(pthread_rwlock_t* rwlock)

读写锁代码实战:

验证一下:三个线程不定时写同一个全局变量,五个线程不定时期读同一全局资源。

test.c文件来操作,内部程序如下:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
 
int number = 0;
 
pthread_rwlock_t lock;
 
void* write_func(void* arg)
{
        while(1)
        {
                pthread_rwlock_wrlock(&lock);
 
                number++;
                printf("write:  %ld,%d\n",pthread_self(),number);
 
                pthread_rwlock_unlock(&lock);
 
                usleep(10);
        }
        return NULL;
}
 
void* read_func(void* arg)
{
        while(1)
        {
                pthread_rwlock_rdlock(&lock);
 
                printf("read:%ld,%d\n",pthread_self(),number);
 
                pthread_rwlock_unlock(&lock);
 
                usleep(10);
        }
 
        return NULL;
}
 
int main()
{
        pthread_rwlock_init(&lock,NULL);
 
        pthread_t p[8];
 
        int i = 0;
 
        for(i = 0;i < 3;i++)
        {
                pthread_create(&p[i],NULL,write_func,NULL);
        }
 
        for(i = 3;i < 8;i++)
        {
                pthread_create(&p[i],NULL,read_func,NULL);
        }
 
        for(i = 0;i < 8;i++)
        {
                pthread_join(p[i],NULL);
        }
 
        pthread_rwlock_destroy(&lock);
 
        return 0;
}

看下面程序运行图,可以看到会读的线程会读取的数据是“写的最后一个线程”所提供的。

13.条件变量

作用:阻塞线程。条件变量不是锁,但是条件变量能够阻塞线程

一般来说 ,使用 条件变量+互斥量 一起来同步线程,其中互斥量:保护一块共享数据;条件变量:引起阻塞。 这两个结合起来就是   生产者和消费者模型

条件变量的两个动作:

  • 条件不满足,阻塞线程
  • 当条件满足,通知阻塞的线程开始工作

条件变量的类型:pthread_cond_t  cond;

主要函数:

13.1 初始化一个条件变量

函数原型:

pthread_cond_init(pthread_cond_t* restrict cond,const pthread_condattr_t* restrict attr);

13.2 销毁一个条件变量

函数原型:

pthread_cond_destroy(pthread_cond_t*  cond);

13.3 阻塞等待一个条件变量

函数原型:

pthread_cond_wait(pthread_cond_t* restrict cond,pthread mutex_t* restrict mutex);

PS: 阻塞线程;将已经上锁的mutex解锁;该函数解除阻塞,对互斥锁加锁

13.4  限时等待一个条件变量

函数原型:

pthread_cond_timedwait( pthread_cond_t* restrict cond , pthread_mutex_t * restrict mutex , const struct timespec* restrict abstime);

13.5 唤醒至少一个阻塞在条件变量上的线程

函数原型:

pthread_cond_signal(pthread_cond_t* cond);

13.6 唤醒全部阻塞在条件变量上的线程

函数原型:

pthread_cond_broadcast(pthread_cond_t* cond);

条件变量代码实战

验证一下:使用条件变量实现生产者,消费者模型。模型图如下:

test.c文件来操作,内部程序如下:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
 
typedef struct node
{
        int date;
        struct node* next;
}Node;
 
Node* head = NULL;
 
pthread_mutex_t mutex;
 
pthread_cond_t cond;
 
void* produce(void* arg)
{
        while(1)
        {
                Node *pnew = (Node*)malloc(sizeof(Node));
 
                pnew->date = rand()%1000;//0-999
 
                pthread_mutex_lock(&mutex);
 
                pnew->next = head;
                head = pnew;
                printf("produce:%ld,%d\n",pthread_self(),pnew->date);
 
                pthread_mutex_unlock(&mutex);
 
                pthread_cond_signal(&cond);
 
                sleep(rand()%3);
        }
        return NULL;
}
 
void* customer(void* arg)
{
        while(1)
        {
                pthread_mutex_lock(&mutex);
 
                if(head == NULL)
                {
                        //continue;
                        pthread_cond_wait(&cond,&mutex);
                }
 
                Node* pdel = head;
                head = head->next;
                printf("customer:%ld,%d\n",pthread_self(),pdel->date);
                free(pdel);
 
                pthread_mutex_unlock(&mutex);
        }
        return NULL;
}
 
int main()
{
        pthread_t p1,p2;
 
        pthread_mutex_init(&mutex,NULL);
        pthread_cond_init(&cond,NULL);
 
        pthread_create(&p1,NULL,produce,NULL);
        pthread_create(&p2,NULL,customer,NULL);
 
        pthread_join(p1,NULL);
        pthread_join(p2,NULL);
 
        pthread_mutex_destroy(&mutex);
        pthread_cond_destroy(&cond);
 
        return 0;
}

通过程序运行图,我们可以看到非常有秩序的依次进行生产者和消费者之间的交替,这就是条件变量代码所实现的功能。

条件变量的核心作用:线程间等待 / 唤醒,实现顺序协作;可以配合锁控制执行流。

二、信号量(补充)

信号量可以看成加强版互斥锁(互斥锁对于共享资源来说 是串行输入,但信号量 对于 共享资源 来说  是并行输入

所需头文件

<semaphore.h>

信号量类型:sem_t sem;

主要函数

1. 初始化信号量

函数原型:

sem_init(sem_t *sem , init pshared , unsigned int value );

参数说明:

pshared: 0-线程同步
                1-进程同步

value:最多有几个线程操作共享数据

2. 销毁信号量

函数原型:

sem_destroy(sem_t *sem)

3.加锁

函数原型:

sem_wait(sem_t * sem)

PS

  •          调用一次相当于对sem做了一个--(减减)操作
  •          如果sem值为0,线程会阻塞

4.尝试加锁

函数原型:

sem_trywait(sem_t * sem)

PS:sem==0,加锁失败,不阻塞,直接返回

5. 限时尝试加锁

函数原型:

sem_timedwait(sem_t * sem,xxxx)

参数说明:

xxxx:代指时间

6. 解锁++

作用:对sem做了++操作

函数原型:

sem_post(sem_t * sem)

三、网络基础

1.网络体系结构

  • 网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。
  • 每层向上层提供服务,同时使用下层提供的服务
  • 网络体系结构即指网络的层次结构和每层所使用的协议的集合
  • 两类非常重要的体系结构:OSI和TCP/IP

其中TCP协议分成了两个不同的协议:

  • 1.用来检测网络传输中差错的传输控制协议TCP
  • 2.专门负责对不同网络进行互联的互联网协议IP

其中OSI模型是一个理想化的模型,尚未有完整的实现。所以OSI模型相关的协议已经很少使用,但模型本身非常通用。OSI模型共有七层,其中二层交换机,三层交换机。

2.TCP/IP 协议族体系

一共有四层,看下表:

 应用层 Relnet , FTP , HTTP , DNS  ,SMTP等
传输层 TCP 和 UDP
 网络层  IP  ICMP   和 IGMP   端到端传输 
 网络接口和物理层 以太网   令牌环网   FDDI   wifi   gps/2g/3g/4g    驱动(屏蔽硬件差异)

3.网络各层的协议解释

(1)网络接口和物理层:mac地址:48位全球唯一,网络设备的身份标识
                                          ARP:地址解析协议,IP地址找到mac地址
                                          ARP: mac地址--找到IP地址
                                          PPP协议:拨号协议(GPRS/3G/4G)

(2)网络层:IP地址
                        IP协议:Internet  protocol(分为IPv4和IPv6)
                        ICMP: Internet控制管理协议,Ping命令属于ICMP
                        IGMP: Internet分组管理协议,广播和组播

(3)传输层:TCP 和 UDP,SCTP

TCP:传输控制协议,提供面向连接的,一对一的可靠传输协议
          数据无误,数据不丢失,数据无失序
          适用场景:适合对传输质量要求较高,以及传输大量数据的通信
                            在需要可靠数据传输的场合,通常适用TCP协议
                           MSN/QQ等即时通讯软件的用户登陆账号管理相关的功能通常采用TCP协议

UDP:用户数据报协议,提供不可靠的,无连接的传输协议
           适用场景:发送小尺寸数据(如对DNS服务器进行IP地址查询时)
                             在接收数据,给出应答较困难的网络适用UDP(如无线网络)
                             适合于广播/组播式通信中
                            MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通常采用UDP协议
                           流媒体,VOD, VoIP , IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输

SCTP:流控制传输协议,TCP的增强版,他能实现多主机,多链路的通信

(4)应用层网页访问协议:HTTP/HTTPS
                       邮件发送接收协议:POP3(收)/SMTP(发),IMAP(可接收邮件的一部分)
                       FTP:文件传输协议
                       elnet/ssh:远程登陆
                       嵌入式相关:NTP:网络时钟协议
                                            SNMP:简单网络管理协议(实现对网络涉及集中式管理)
                                            RTP/RSTP:用于传输音视频的协议(安防监控)

4.网络的封包和拆包

TCP/IP协议下的数据包

5.网络预备知识

5.1 socket

socket是一个编程接口,是一个特殊的文件描述符(对他执行IO的操作函数,比如read,write,close等),并不仅限于TCP/IP协议,面向连接TCP,无连接UDP;

socket代表网络编程的一种资源;

分类

  1. 流式套接字(SOCK_STREAM,唯一对应TCP;提供了一个面向连接,可靠的数据传输服务,数据无差错,无重复的发送顺序接受。内涉及流量控制,避免数据淹流没慢的接收方。数据被看作式字节流,无长度限制。
  2. 数据包套接字(SOCK_DGRAM)。唯一对应UDP;提供无连接服务器,数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
  3. 原始套接字(SOCK_RAW)。对应多个协议,发送穿透了传输层;可以对较低层次协议如IP,ICMP直接访问。(直接跳过传输层)

5.2 IP地址

IP地址是Internet中主机的标识,Internet中的主机要与别的机器通信必须具有一个IP地址,IP地址为32位(IPv4)或者128位(IPv6),每个数据包都必须携带目的IP地址和源IP地址。路由器依靠此信息为数据包选择路由器

表示的形式:常用点分形式,如202.38.64.10,最后都会转化成一个32位的无符号整数

mobile IPV6:local IP(本地注册的IP),roma IP(漫游IP)

特殊IP地址

  • 局域网IP:192.xxx.xxx.xxx    10.xxx.xxx.xxx
  • 广播IP:xxx.xxx.xxx.255   255.255.255.255(全网广播)      网络风暴 
  • 组播IP:224.xxx.xxx.xxx   -239.xxx.xxx.xxx

5.3 端口号

端口号一般为16位数字,1-65535

目的:为了区分一台主机接收到的数据包应该转交给哪个任务进程处理,使用端口号来区别

预留端口:1-1023(FTP:24,SSH:22,HTTP:80,HTTPS:469)

保留端口:1024-5000(不建议使用)

可以使用的端口:5000-65535

TCP端口号UDP端口号独立

网络里的通信是由 IP地址+端口号 来决定

5.4 字节序

字节序是指不同的CPU访问内存中的多字节数据时候,存在大小端的问题

大小端:

  • 小端:低端内存存放着低端数据
  • 大端:低端内存存放着高端数据

如果CPU访问的是字符串,则不存在大小端问题

一般来说X86/ARM小端模式

power/mios/arm作为路由器时,大端模式

网络传输的时候采用大端模式

5.5  字节转换函数

作用:把给定系统所采用的字节序成为主机字节序,为了避免不同类别主机之间在数据交换时由于对于字节序的不同而导致的差错,引入了网络字节序

 主机字节序到网络字节序:
  • u_long htonl(u_long hostlong);
  • u_short htons(u_short short);
 网络字节序到主机字节序:
  • u_long ntohl(u_long hostlong)
  • u_short ntohs(s_short short)

5.6   IP地址的转换

man手册可以查看该函数详情,一般用一下几个函数:

<1> inet_aton()函数

作用:将strptr所指的字符串转换成32位的网络字节序二进制值

<2>  inet_addr()函数

功能同 inet_aton()函数,返回转换后的地址

仅适用于IPV4,出错时返回-1

局限性:不能用于255.255.255.255的转换

<3> inet_ntoa()函数

作用:将32位网络字节序二进制地址转换成点分十进制的字符串

<4> inet_pton()函数

作用:将IPV4/IPV6的地址转换成binary格式;适用于IPV4/IPV6; 能正确处理255.255.255.255的转换问题

函数原型:

int inet_pton(int af,const char* src,void* dst)

参数说明:

1.地址协议族(AF_INET或AF_INET6)

2.src:是一个指针(填写分点形式的IP地址(主要指IPV4))

3.dst:转换的结果给到dst

<5>  inet_ntop()函数

作用:将IPV4/IPV6的网络字节序变成本地的字符串形式的IP地址;适用于IPV4/IPV6; 能正确处理255.255.255.255的转换问题

但一般用于接收

函数原型:

int inet_ntop(int af, const void* src, char* dst, socklen_t size)

参数说明:

1.地址协议族(AF_INET或AF_INET6)

2.src:是一个指针(填写分点形式的IP地址(主要指IPV4))

3.dst:转换的结果给到dst

四.TCP编程

一般客户端(client)是 主机到网络

一般服务端(service)是 网络到主机

下图为TCP编程的流程图:我们在这一章将会对这些函数进行讲解,并进行代码复现。

1.socket函数

所需头文件:

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

函数原型:

int socket(int domain, int type ,int protocol);

返回值:

  •              成功:返回文件描述符
  •              错误:返回-1

参数说明:

int domain:AF_INET:IPv4 Internet protocols 
                     AF_INET6:IPv6 Internet protocols 
                     AF_UNIX ,AF_LOCAL :Local communication
                     AF_NETLINK :kernel user interface device
                    AF_PACKET :low level packer interface

type :SOCK_STREAM:流式套接字。唯一对应TCP
            SOCK_DGRAM :数据包套接字,唯一对应UDP
           SOCK_RAM : 原始套接字 

protocol:一般填0,原始套接字编程时需填充

2.bind绑定函数

作用:绑定服务器:TCP地址和端口号

函数原型:

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

返回值:

  •              成功:0
  •              错误:返回-1

参数说明:

sockfd :通过socket()函数拿到的fd

addr :采用struct socket的结构体地址
            struct sockaddr{    //通用结构体
                                       unsigned short sa_family ; //2个字
                                       char  sa_data[14];                //14字节的协议地址
                                      }
            truct sockaddr_in{    //基于socket通信结构体--------后面要将这个结构体转换成 通用结构体
                                           sa_family_t sin_family ; //两个字节
                                           in_port_t    sin_port; //两个字节
                                           struct in_addr sin_addr; //四个字节(是下面的那个结构体)
                                           sin zero[8];//八位,填充字节,需清零(清零函数bzero)
                                          }
                        struct in_addr{

                                                uint32_t s_addr,//32位网络字节序
                                               }

addrlen:地址长度

3.listen函数

 函数原型:

int listen(int sockfd ,int  backlog )

返回值:

  •              成功:0
  •              错误:-1

参数说明:

sockfd:通过socket()函数拿到的fd;

backlog:同时允许几路客户端和服务器进行正在连接的过程(正在三次握手),一般填5

内核中服务器的套接字fd会维护2个链表:

  • 1.正在三次握手的客户端链表(数量=2*backlog+1)
  • 2.已经建立好连接的客户端链表(已经完成三次握手分配好了的newfd)

example

  • listen(fd,5) 表示系统允许11个客户端进行三次握手

4.accept()函数

作用:阻塞等待客户端连接请求

函数原型:

int accept(int sockfd ,struct sockaddr *addr, socklen_t  *addrlen)   

返回值:

  •              成功:返回已经建立连接的新的newfd
  •              错误:返回-1

参数说明:

sockfd:经过前面socket()创建并通过bind(),listen()设置过的fd

addr:指向存放地址信息的结构体的首地址

获取客户端IP地址和端口号

addrlen:存放地址信息的结构体的大小

5.connect()函数

作用:客户端连接函数

 函数原型:

int connect(int sockfd,  struct sockaddr* serv_addr , int addrlen)

返回值:

  •              成功:0
  •              错误:返回-1

参数说明:

sockfd:通过socket()函数拿到的fd

addr:struct sockaddr的结构体变量地址

addrlen:地址长度

6.send()函数

作用:类似于write

 函数原型:

ssize_t send(int sockfd ,const void *buf ,size_t len ,int flags);

返回值:

  •              成功:实际发送的字节数
  •              错误:-1,并设置errno

参数说明:

sockfd: socket函数返回的fd

buffer:发送缓冲区首地址

length:发送的字节

flags:发送方式(通常为0),作用和write一样
           MSG_DONTWAIT,非阻塞
            MSG_OOB:用于TCP类型的带外数据(out of band)

7.recv()/read()函数

作用:网络中接收数据

函数原型:

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

返回值:

  •              成功:将返回消息或数据报的长度 (以字节计)
  •    连接已关闭:0
  •              错误:-1

参数说明:

flag:一般填0,和read作用一样
特殊的标志:MSG_DONTWAIT
                     MSG_OOB:读取带外数据
                     MSG_PEEK:流----------可以用来校验

8.receivefrom()函数

作用:阻塞等待客户端数据

函数原型:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

返回值:

  •              成功:实际接收的字节数
  •              错误:-1,并设置errno

参数说明:

sockfd:socket函数返回的fd

buf:接收缓冲区首地址

len:接收缓冲区的最大长度

flags:接收方式(通常为0)

  • MSG_DONTWAIT:非阻塞接收
  • MSG_PEEK:查看数据但不从缓冲区移除
  • MSG_WAITALL:等待所有请求的数据到达

src_addr:存放发送方地址信息的结构体指针

addrlen:输入输出参数,指向地址结构体大小的变量
 

9.sendto()函数

作用:指定服务器的IP地址和端口号,要发送的数据

 函数原型:

size_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

返回值:

  •              成功:实际发送的字节数
  •              错误:-1,并设置errno

参数说明:

sockfd:socket函数返回的fd

buf:发送缓冲区首地址

len:发送的字节数

flags:发送方式(通常为0)

  • MSG_DONTWAIT:非阻塞发送
  • MSG_NOSIGNAL:发送失败时不产生SIGPIPE信号
  • dest_addr:目标地址信息的结构体指针

addrlen:目标地址结构体的长度

TCP循环服务器模型代码实战:

首先确定一下我们内网IP,我们输入ifconfig指令确定一下:

确定好这个,那么我们的代码中的define ip地址就可以确定了,如果打不开ifconfig,输入ifconfig,会弹出一个安装包,然后下载就好。

client.c文件来实现我们客户端写入内容的功能,内部程序如下:

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
 
#define QUIT_STR "QUIT"
#define BUFSIZE 1024
#define SERVER_PORT 5001
#define SERVER_IP_ADDR "192.168.186.128"
 
int main()
{
        int fd = -1;
 
        fd = socket(AF_INET,SOCK_STREAM,0);
        if(fd < 0)
        {
                perror("socket");
                exit(1);
        }
 
        struct sockaddr_in sin;
 
        bzero(&sin,sizeof(sin));
 
        sin.sin_family = AF_INET;
        sin.sin_port = htons(SERVER_PORT);
        sin.sin_addr.s_addr = inet_addr(SERVER_IP_ADDR);
 
        if(connect(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0)
        {
                perror("connect");
                exit(1);
        }
 
        char buf[BUFSIZE];
 
        while(1)
        {
                bzero(buf,BUFSIZE);
 
                if(fgets(buf,BUFSIZE - 1,stdin) == NULL)
                {
                        continue;
                }
 
                write(fd,buf,strlen(buf));
 
                if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
                {
                        break;
                }
        }
 
        close(fd);
 
        return 0;
}

service.c文件来实现我们服务端读取内容的功能,内部程序如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
 
#define QUIT_STR "QUIT"
#define BUFSIZE 1024
#define BACKLOG 5
#define SERV_IP 5001
#define SERV_IP_ADDR  "192.168.186.128"
 
int main()
{
        int fd = -1;
 
        //socket
        fd = socket(AF_INET,SOCK_STREAM,0);
        if(fd < 0)
        {
                perror("socket");
                exit(1);
        }
 
        struct sockaddr_in sin;
        bzero(&sin,sizeof(sin));
 
        sin.sin_family = AF_INET;
        sin.sin_port = htons(SERV_IP);
        sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR);
 
        /*
           if(inet_pton(AF_INET,SERV_IP_ADDR,(void *)&sin.sin_addr.s_addr) != 1)
           {
           perror("inet_pton");
           exit(1);
           }
           */
 
        //bin
        if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)) <0)
        {
                perror("bind");
                exit(1);
        }
 
        //listen
        if(listen(fd,BACKLOG) < 0 )
        {
                perror("listen");
                exit(1);
        }
 
        //accept
        int newfd = -1;
        newfd = accept(fd,NULL,NULL);
        if(newfd < 0)
        {
                perror("accept");
                exit(1);
        }
 
        //read
        char buf[BUFSIZE];
        int ret = -1;
 
        while(1)
        {
                do
                {
                        bzero(buf,BUFSIZE);
                        ret = read(newfd,buf,BUFSIZE-1);
                }while(ret < 1);
 
                if(ret < 0)//出错退出
                {
                        exit(1);
                }
                if(!ret)//ret == 0,没有读取到客户端发送的消息
                {
                        break;
                }
 
                printf("receive date is:%s\n",buf);
 
                if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
                {
                        printf("Client is exiting!\n");
                        break;          }
        }
 
        close(fd);
        close(newfd);
 
        return 0;
}

然后再打开一个终端,现在两个终端,一个作为服务端,一个作为客户端,程序运行图如下:

TCP多进程并发服务器代码实战:

TCP并发服务器的思想是每一个客户机的请求并不由服务器直接处理,而是由服务器创建一个子进程来处理。

client.c文件来实现我们客户端写入内容的功能,内部程序如下:

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
 
#define QUIT_STR "QUIT"
#define BUFSIZE 1024
#define SERVER_PORT 5001
#define SERVER_IP_ADDR "192.168.186.128"
 
//./client "192..." 5003
 
int main(int argc,char*argv[])
{
        int fd = -1;
 
        if(argc != 3)
        {
                exit(1);
        }
 
        int port = -1;
        port = atoi(argv[2]);
        
        //1.socket
        fd = socket(AF_INET,SOCK_STREAM,0);
        if(fd < 0)
        {
                perror("socket");
                exit(1);
        }
 
        //2.connect
        struct sockaddr_in sin;
 
        bzero(&sin,sizeof(sin));
 
        sin.sin_family = AF_INET;
        sin.sin_port = htons(port);
        sin.sin_addr.s_addr = inet_addr(argv[1]);
 
        if(connect(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0)
        {
                perror("connect");
 
                exit(1);
        }
    
        //write
        char buf[BUFSIZE];
 
        while(1)
        {
                bzero(buf,BUFSIZE);
 
                if(fgets(buf,BUFSIZE - 1,stdin) == NULL)
                {
                        continue;
                }
 
                write(fd,buf,strlen(buf));
 
                if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
                {
                        break;
                }
 
        }
 
        close(fd);
 
        return 0;
 
}

service.c文件来实现我们服务端读取内容的功能,内部程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string.h>
#include <pthread.h>
 
#define SERVER_PORT 5004
#define SERVER_ADDR_IP "192.168.186.128" 
#define BACKLOG 5
#define BUFSIZE 1024
#define QUIT_STR "QUIT"
 
void * client_date_handle(void* arg);
 
int main()
{
        //1.socket
        int fd = -1;
 
        fd = socket(AF_INET,SOCK_STREAM,0);
        if(fd < 0)
        {
                perror("socket");
        }
 
        struct sockaddr_in sin;
 
        sin.sin_family = AF_INET;
        sin.sin_port = htons(SERVER_PORT);
        //sin.sin_addr.s_addr = inet_addr(SERVER_ADDR_IP);
        sin.sin_addr.s_addr = INADDR_ANY;
 
        //2.bind
        if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0)
        {
                perror("bind");
                exit(1);
        }
 
        //3.listen
        if(listen(fd,BACKLOG) < 0)
        {
                perror("listen");
                exit(1);
        }
 
        //4.accept
 
        /*
           int newfd = -1;
           newfd = accept(fd,NULL,NULL);
           if(newfd < 0)
           {
           perror("accept");
           }
        */
 
        pthread_t pid;
 
        int newfd = -1;
        struct sockaddr_in cin;
        socklen_t addrlen = sizeof(cin);
 
        while(1)
        {
                newfd = accept(fd,(struct sockaddr *)&cin,&addrlen);
                if(newfd < 0)
                {
                        perror("accept");
                        exit(1);
                }
                
                //打印ipv4的地址和端口号
 
                char ipv4_addr[14];
                if(!inet_ntop(AF_INET,(void *)&cin.sin_addr,ipv4_addr,sizeof(cin)))
                {
                        perror("pton");
                        exit(1);
                }
                printf("Clinet:(%s,%d)id connnect!\n",ipv4_addr,cin.sin_port);
                
                //创建线程,newfd要传输
                pthread_create(&pid,NULL,client_date_handle,(void *)&newfd);
        }
 
        close(fd);
 
        return 0;
}
 
void* client_date_handle(void* arg)
{
        char buf[BUFSIZE];
        int ret = -1;
        int newfd = *(int *)arg;
        printf("newfd is :%d\n",newfd);
 
        while(1)
        {
                do
                {
                        bzero(buf,BUFSIZE);
                        ret = read(newfd,buf,BUFSIZE-1);
                }
                while(ret < 1);
 
                if(ret < 0)
                {
                        exit(1);
                }
 
                if(!ret)
                {
                        break;
                }
 
                printf("receive date is: %s\n",buf);
 
                if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
                {
                        printf("Client is exiting\n");
                        break;
                }
        }
 
        close(newfd);
        return NULL;
}

然后再打开一个终端,现在两个终端,一个作为服务端,一个作为客户端,,其中服务端编译的时候,因为有线程函数,因此我们要加入“-pthread”,客户端要在执行程序的时候,后面加入ip号和端口,程序运行图如下:

最后可以看到 我们输入了一个“quit”就退出了,这很符合我们的代码

五、UDP编程

无连接尽力传输,UDP:是不可靠传输

实时的音视频传输,DNS域名解析包

下图为UDP编程的流程图

UDP代码实战:

client.c文件来实现我们客户端写入内容的功能,内部程序如下:

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
#define BUFSIZE 1024
#define QUIT_STR "QUIT"
 
int main(int argc,char * argv[])
{
        if(argc != 3)
        {
                exit(1);
        }
 
        int fd = -1;
        fd = socket(AF_INET,SOCK_DGRAM,0);
        if(fd < 0)
        {
                perror("socket");
                exit(1);
        }
 
        int port = -1;
        port = atoi(argv[2]);
 
        struct sockaddr_in sin;
 
        bzero(&sin,sizeof(sin));
 
        sin.sin_family = AF_INET;
        sin.sin_port = htons(port);
        sin.sin_addr.s_addr = inet_addr(argv[1]);
 
        char buf[BUFSIZE];
        bzero(&buf,BUFSIZE);
 
        while(1)
        {
                if(fgets(buf,BUFSIZE-1,stdin) == NULL)
                {
                        perror("fgets");
                        continue;
                }
 
                if(sendto(fd,buf,strlen(buf),0,(struct sockaddr*)&sin,sizeof(sin)) < 0)
                {
                        perror("sendto");
                        break;
                }
 
                if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
                {
                        printf("Cilet is exiting!\n");
                        break;
                }
        }
 
        close(fd);
 
        return 0;
}

service.c文件来实现我们服务端读取内容的功能,内部程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
 
#define SERVER_PORT 5001
#define BUFSIZE 1024
#define QUIT_STR "QUIT"
 
int main()
{
        int fd = -1;
        fd = socket(AF_INET,SOCK_DGRAM,0);
        if(fd < 0)
        {
                perror("socket");
                exit(1);
        }
 
        int b_reuser = 1;
 
        setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuser,sizeof(int));
 
        struct sockaddr_in sin;
        sin.sin_family = AF_INET;
        sin.sin_port = htons(SERVER_PORT);
        sin.sin_addr.s_addr = INADDR_ANY;
 
        if(bind(fd,(struct sockaddr*)&sin,sizeof(sin)) < 0)
        {
                perror("bind");
                exit(1);
        }
 
        char buf[BUFSIZE];
        struct sockaddr_in cin;
        socklen_t addrlen = sizeof(cin);
 
        while(1)
        {
                bzero(buf,BUFSIZE);
                if(recvfrom(fd,buf,BUFSIZE-1,0,(struct sockaddr*)&cin,&addrlen) < 0)
                {
                        perror("recvfrom");
                        continue;
                }
 
                char ipv4_addr[16];
                if(!inet_ntop(AF_INET,(void *)&cin.sin_addr,ipv4_addr,sizeof(cin)))
                {
                        perror("inet_pton");
                        continue;
                }
                printf("Receive client(%s,%d):%s\n",ipv4_addr,ntohs(cin.sin_port),buf);
 
                if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
                {
                        printf("Client(%s,%d) is exiting!\n",ipv4_addr,ntohs(cin.sin_port));
                }
 
        }
 
        close(fd);
 
        return 0;
}

然后再打开一个终端,现在两个终端,一个作为服务端,一个作为客户端,,其中客户端要在执行程序的时候,后面加入ip号和端口,程序运行图如下:

最后可以看到 我们输入了一个“quit”就退出我们的客户端了,这很符合我们的代码

六、I/O多路复用

1.I/O模型

在unix/linu下主要有四种I/O模式

1.1 阻塞I/O

阻塞I/O:最常用
               大部分程序使用的都是阻塞模式的I/O
                缺省情况下,套接字建立后所处于的模式就是阻塞I/O模式
                读操作:read,recv,recvfrom
                写操作:write,send
                其他操作:accept,connect

以read函数为例:

  • 进程调用read函数从套接字上读取数据,当套接字的接收缓冲区中还没有数据可读,函数read将发送阻塞
  • 他会一直阻塞下去,等待套接字的接收缓冲区中有数据可读
  • 经过一段时间后,缓冲区内接收到数据,于是内核便去唤醒该进程,通过read访问这些数据
  • 如果在进程阻塞过程中,对方发生故障,那这个进程将永远阻塞下去
  • UDP不用等待确认,因为没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况,在UDP套接字上执行的写操作永远都不会阻塞

 写阻塞:

  • 在写操作时发送的情况要比读操作少。主要发生在要写入的缓冲区的大小小于要写入的数据量的情况下。
  •  这时,写操作不进行任何拷贝工作,将发送阻塞
  • 一旦发送缓冲区内有足够多的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。
  • UDP不用等待确认没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况,在UDP套接字上执行的写操作永远都不会阻塞,举例sendto

1.2 非阻塞I/O

  1. 可防止进程阻塞在I/O操作上,需要轮询
  2. 当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核“当我请求的I/O操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我”
  3. 当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停的测试是否一个文件描述符有数据可读(polling)
  4. 应用程序不停的polling内核来检查是否I/O操作已经就绪,这将是一个极浪费CPU资源的操作

这种模式使用中不普遍:

  • fcntl()函数:  当你一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞IO模式。可以使用函数fcntl()设置一个套接字的标志位为O_NOBLOCK来实现非阻塞
  • ioctl()函数

fcntl()函数原型:

int fcntl(int fd, int cmd, long arg) 

ioctl()函数原型:

ioctl(sock_fd, FIONBIO,&b_on); 

1.3  I/O多路复用

概念:允许同时对多个I/O进行控制

基本常识:linux中每个进程最多可以打开1024个文件,最多有1024个文件描述符

文件描述符的特点:

  • 非负整数
  • 从最小可用的数字来分配
  • 每个进程启动时默认打开0、1、2三个文件描述符
  • 注:多路复用针对不止套接字fd,也针对普通的文件描述符fd

为什么要用“I/O多路复用”:

  1. 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;
  2. 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间
  3. 若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,是程序变得更加复杂

比较好的方法是使用IO多路复用,其基本思想是:

  • 先构造一张有关描述符的表,然后调用一个函数,当这些文件描述符中的一个或多个已准备好进行IO时函数才返回
  • 函数返回时告诉进程那个描述符已就绪,可以进行IO操作

下图为IO多路复用流程图

多路复用IO操作使用

  1. 把关心的文件描述符放入到集合中fd_set
  2. 调用select()/poll函数去监控集合fd_set中 有哪些文件描述符发生了阻塞,并且等待集合中一个或多个文件描述符有数据
  3. 有数据时,退出select()阻塞
  4. 以此判断哪个文件描述符有数据
  5. 依次处理有数据的文件描述符上的数据
1.3.1 fd_set
  • void FD_zero(fd_set * fdset) //对集合清0
  • void FD_set(int fd, fd_set  *fdset) //把fd加入集合
  • void FD_CLR(int fd, fd_set *fdset )//从集合中清楚fd
  • void FD_ISSET(int fd, fd_set *fdset )//判断fd是否在fd_set中,如果fd为0.则代表stdin
1.3.2 select()

看man手册

 int select(int maxfdp,        //maxfd+1
                fd_set* readfds              //读集合
                fd_set* writefds              //写集合
                fd_set* errorfds              //异常集合
                struct timeval* timeout                         //超时                                                                                     )

一般:填写集合,写集合填空,异常集合(带位数据)

超时:

struct  timeval{ 

        long  tv_sec;   //秒 

        long tv_usec;   //微妙 

}; 

select退出后:集合表示有数据的集合

if(FD_ISSET(fd,set)) 

        //1.如果监听套接字有数据,新的客户端进行连接,则accept 

        //2.若建立连接的套接字有数据,则去读read 

I/O多路复用代码实战:

记得要加入头文件!!!

//例子
int main(void) 
{ 
    struct timeval tout; 
    fd_set rset; 
    int maxfd = ‐1; 
 
    fd = socket(...); 
    bind(fd,...); 
    listen(fd,...); 
 
    while(1) 
    { 
        maxfd = fd; 
        FD_ZERO(&rset); 
        FD_SET(&rset); //依次把已经建立好连接fd加入到集合中,记录最大的文件描述符 
        tout.tv_sec = 5; 
        tout.tv_user = 0; 
        select(maxfd+1,&rset,NULL,NULL,&tout); //阻塞 
 
        if(FD_ISSET(fd,&rset)) //有1个或者多个文件描述符有数据 
        { 
            newfd = accept(fd,...); 
            //依次判断已建立连接的客户端是否有数据 
            //xxx 
        } 
    } 
} 

1.4  信号驱动I/O

信号驱动I/O 是 一种异步通信模型:SIGIO

七、TCP/IP协议原理

下面这个是TCP/IP协议原理图

三次握手、四次挥手:

  • 一定要标注客户端和服务器
  • 三次握手的连接必须是有客户端发起的(四次握手 客户端和服务器都可以发起)
  • SYN,ACK,FIN等标志符号应该写上
  • SYN:同步位,用于发起连接和同步序列号
  • ACK:确认位,用于确认收到的数据
  • seq:序列号,标识发送字节流的位置
  • ack:确认号,期望收到的下一个字节的序列号(对方 Seq+1)
  • FIN - Finish (结束)

方便记忆:

第一次握手:

SYN=1    → "我想跟你通信!"
seq=x    → "我的第一封信编号是x"

第二次握手:

SYN=1    → "我也愿意通信!"
ACK=1    → "这是回信确认"
seq=y    → "我的第一封信编号是y" 
ack=x+1  → "收到了你的x号信,期待x+1号"

第三次握手:

ACK=1    → "确认收到你的信"
seq=x+1  → "我现在发x+1号信"
ack=y+1  → "收到了你的y号信,期待y+1号"

第一次挥手:

FIN=1    → "我要结束通信了!"
seq=u    → "我最后发送的数据编号是u"

第二次挥手:

ACK=1    → "收到你的结束请求"
ack=u+1  → "确认收到了你的u号数据,期待u+1号"

第三次挥手:

FIN=1    → "我也要结束通信了!"
ACK=1    → "这是确认回复" 
seq=v    → "我最后发送的数据编号是v"
ack=u+1  → "再次确认你的结束请求,期待u+1号"

第四次挥手:

ACK=1    → "收到你的结束请求"
seq=u+1  → "我的下一个数据编号应该是u+1"
ack=v+1  → "确认收到了你的v号数据,期待v+1号"

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐