欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

TCP/IP网络编程基础阅读笔记

程序员文章站 2022-03-04 22:13:04
...

TCP/IP网络编程基础阅读笔记

open函数

该函数打开一个文件(linux下任何皆文件),返回文件描述符,失败返回-1

int open(const char* pathname,int flags,mode_t mode)
flags:文件打开方式的标志
O_RDONLY:只读方式打开
O_WRONLY:只写方式打开
O_RDWR: 可读可写方式打开
O_CREAT:若打开的文件不存在则创建文件
O_APPEND:追加方式打开
O_TRUNC:若文件存在且可写方式打开则将文件长度清0
mode:打开文件的存取权限,只有在创建文件时才有效。也就是创建文件时文件的权限
close函数

关闭文件描述符打开的文件

#include<unistd.h>
int close(int fd)
read函数

从文件读取指定数量的字节数据存入传入的buf区域

#include<unistd.h>
ssize_t read(int fd, void *buf, size_t count)
fd:文件描述符
buf:指定用来存储所读数据的缓冲区
count:指定要读取的字节数

返回实际读取的字节数,返回0代表到达文件结尾,返回-1调用出错
write函数
#include<unistd.h>
ssize_t write(int fd,void *buf, size_t count)
同write函数
lseek函数
off_t lseek(int fd, off_t offset, int whence)
offset:偏移量,可正可负,指相对于当前的偏移量
whence:表示当前基点位置
    SEEK_SET:基点为当前文件的开头
    SEEK_CUR:基点为当前文件指针的位置
    SEEK_END:基点为当前文件的文件末
ioctl函数

函数用于设置I/O的特性

#include<sys/ioctl.h>
int ioctl(int fd,int cmd,...)

通用地址存储结构
struct sockaddr{
    u_char sa_len;//sockaddr的长度
    u_short sa_family;//地址族
    char sa_data[14];//14字节协议端点信息
};
#include<netinet/in.h>
struct sockaddr_in{
    short int sin_family;//地址族
    unsigned short int sin_port;//端口号
    struct in_addr sin_addr;//存储ip地址的结构
    unsigned char sin_zero[8];//填充0以保持sockaddr相同大小
};
ipv4地址结构
struct in_addr{
    unsigned long s_addr;//ip地址
};
htons函数

将主机字节顺序转换为网络字节顺序(host-to-network-for type short)

uint16_t htons(uint16_t hostshort)
htonl函数

将主机字节顺序转换为网络字节顺序(host-to-network-for type long)

uint32_t htonl(uint32_t hostlong)
ntohs函数

将网络字节顺序转换为主机字节顺序(network-to-host-for type short)

uint16_t ntohs(uint16_t netshort)
ntohl函数

将网络字节顺序转换为主机字节顺序(network-to-host-for type long)

uint32_t ntohl(uint32_t netlong)
inet_addr函数
#include<arpa/inet.h>
in_addr_t inet_addr(const char *cp)
将点分十进制的ip地址转换为32位二进制表示的网络字节顺序的地址。出错返回-1
#include<arpa/inet.h>
int inet_aton(const char*cp, struct in_addr *inp)
将一个用点分十进制的in_addr结构转换为二进制后存储在inp中
inp:一个用二进制表示的32位的ip地址结构
#include<arpa/inet.h>
char * inet_ntoa(struct in_addr *inp)
将二进制表示的ip地址转换为点分十进制的地址
gethostbyname函数

通过域名查询ip地址

#include<netdb.h>
struct hostent * gethostbyname(const char *name)

struct hostenv{
    char *h_name;//主机名
    char **h_aliases;//主机别名
    char h_addrtype;//主机ip地址类型ipv4(AF_INET),ipv6(AF_INET6)
    char h_length;//ip地址长度
    char ** h_addr_list;//以网络字节顺序存储主机ip地址列表(一个主机可能有多个ip地址)
};
getservbyname函数

通过服务名查找端口号

#include<netdb.h>
struct servent * getservbyname(const char *name,const char * proto)

struct servent{
    char *s_name;//主机名
    char * *s_aliases;//主机别名
    short s_port;//端口
    char * s_proto;//协议名
};
getprotobyname函数

根据协议名查找协议号

#include<netdb.h>
struct protoenv * getprotobyname(const char *name)
struct protoenv{
    char *p_name;//协议名
    char * * s_aliases;//主机别名
    int p_proto;//协议号
};

套接字API

socket函数 TCP/UDP

创建套接字,返回套接字描述符。

#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol)
domain:协议族。通常赋值为PF_INET,表示TCP/IP协议族
    AF_INET:ipv4协议(和PF_ANET同值)
    AF_INET6:ipv6协议
    AF_LOCAL:Unix域协议
    AF_ROUTR:路由套接字
    AF_KEY:**套接字
type:SOCK_STREAM (TCP)、SOCK_DGRAM (UDP)、SOCK_RAW (原始套接字)
protocol:协议号。通常赋值为0。在前两种无法区分协议时通过这个参数来区分
connect函数 TCP

用于配置socket并与远端服务器建立一个TCP连接

#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd, struct sockaddr * serv_addr, int addrlen)
serv_addr:服务端地址
addrlen:sockaddr结构的长度 sizeof运算符计算
bind函数 TCP/UDP

用于将socket和本地端点地址相关联,调用成功返回一个整型数值,失败返回-1

#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr,int addrlen)
my_addr:指向本地端点地址的结构。
addrlen:sizeof(struct sockaddr) 计算得到
listen函数 TCP

用于将socket处于被动的监听状态,并为该socket建立一个输入数据队列。

#include<sys/types.h>
#include<sys/socket.h>
int listen(int sockfd, int backlog)
backlog:指定允许在等待队列中所允许的连接数。
accept函数 TCP

从等待队列中抽取第一个连接,并为该连接创建一个新的套接字。成功返回一个新的套接字描述符,失败返回-1。若为阻塞模式,accept函数将等到等待队列中有连接时才返回。

#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr * addr, socklen_t *addrlen)
addr:用于存储接受到连接的套接字地址
addrlen:远端地址的长度。(注意时传入的指针)
send函数 TCP

用于给TCP连接的另一端发送数据。成功返回实际发送的字节数,失败返回-1

#include<sys/types.h>
#include<sys/socket.h>
ssize_t send(int sockfd,const void * buf, size_t len, int flags)
buf:要发送的数据的buf
len:发送的数据长度
flags:调用方式,一般传0
recv函数 TCP

从TCP连接的另一端获取传过来的数据。成功返回读取的实际字节数,失败返回-1。

#include<sys/types.h>
#include<sys/socket.h>
int recv(int sockfd, void *buf, int len, unsigned int flags)
同send
sendto函数 UDP

用于UDP发送数据。成功返回实际发送的字节数,失败返回-1

#include<sys/types.h>
#include<sys/socket.h>
int sendto(int sockfd,const void * msg,int len,unsigned int flags,const struct sockaddr * to,int to_len)
msg:发送的数据的缓冲区
len:缓冲区长度
flags:一般设置为0
to:对端端点地址
to_len:对端端点地址的长度
recvfrom函数 UDP

用于UDP接收数据。成功返回实际接收的字节数,失败返回-1

#include<sys/types.h>
#include<sys/socket.h>
int recvfrom(int sockfd, void *buf,int len,unsigned int flags,struct sockaddr * from,int fromlen)
同sendto
close函数 TCP/UDP

关闭套接字。成功返回0,失败返回-1

#include<unistd.h>
#include<sys/socket.h>
int close(int sockfd)
shutdown函数

用于套接字某个方向上关闭数据传输,而另一个方向上的传输任然可以继续进行

#include<sys/types.h>
#include<sys/socket.h>
int shutdown(int sockfd,int howto)
howto:
    0:仅关闭读。套接字不再接收任何数据,且丢弃当前缓冲区的所有数据
    1:仅关闭写。套接字将缓冲区的数据发送完后,进程将不能够再对该套接字调用写函数
    2:同时关闭读写。和close函数类似。与close不同的是当多个进程共享套接字的时候如果调用shutdown函数,那么所有的进程都会受到影响。close函数则只会影响调用的那个进程
getpeername函数

获取连接的远端对等套接字的名称。成功返回0,失败返回-1

#include<sys/types.h>
#include<sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int * addrlen)
addr:用于存储远端对等套接字的端点地址
addrlen:sizeof(struct sockaddr)获得
setsockopt函数

设置套接字的相关参数,成功返回0,失败返回-1

#include<sys/types.h>
#include<sys/socket.h>
int setsockopt(int sockfd,int level, int optname, const void *optval,socklen_t optlen)
level:指定选项所在协议层。为了设置套接字层选项,设置为SOL_SOCKET。
optname:选项名。如下表
optval:选项的值
optlen:选项值的长度
选项名称 说明 数据类型
SO_BROADCAST 允许发送广播数据 int
SO_DEBUG 允许调试 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 获得套接字错误 int
SO_KEEPALIVE 保持连接 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SO_REUSERADDR 允许重用本地地址和端口 int
SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int

注意:设置套接字缓冲区大小时要注意函数调用顺序,因为设置的参数生效时是在套接字建立连接的时候,建立连接就需要协商两边的窗口等信息。所以客户端要在connect函数之前调用,服务端要在listen函数之前调用

getsockopt函数

获取套接字的参数信息,成功返回0,出错返回-1

#include<sys/types.h>
#include<sys/socket.h>
int getsockopt(int sockfd,int level, int optname, const void *optval,socklen_t optlen)
同setsockopt函数

相关辅助函数

memset函数

对一段内存进行赋值或者清空

#include<mem.h>
void * memset(void *desmem,int val,size_t n)
desmem:操作的内存地址
val:要被赋予的值
n:要赋值的长度,单位字节
atoi函数

将字符串转化为整型数值,成功返回转换后的值,失败返回0

#include<stdlib.h>
int atoi(const char *str)
time函数

获取当前时间戳。成功返回GTM自1970年1月1日 00:00:00以来的秒数,失败返回0

#include<time.h>
time_t time(time_t *time)
time:用来存储获取到的结果的指针。也可用返回值来接收结果
ctime函数

用于将日历时间转换为字符串形式的本地时间

#include<time.h>
char *ctime(const time_t * timer)
timer:存储当前日历时间的指针,一般由time()函数获得。
strerror函数

返回错误编号对应的错误原因的字符串描述结果

#include<string.h>
char * strerror(int errnum)
errnum:错误编号
可变参数函数
#include<stdarg.h>
void va_start(va_list ap,argN)
void va_copy(va_list *dest,va_list src)
type va_arg(va_list ap,type)
void va_end(va_list ap)

使用范例

#include<stdarg.h>
#include<stdio.h>
int add(int a1,...);
int main()
{
    int a1 = 1,a2 = 2,a3 = 3,a4 = 4,end = -1;
    printf("sum is %d\n",add(a1,a2,a3,a4,end));

}
int add(int a1, ...)
{
    va_list args;
    int sum = a1;
    va_start(args,a1);
    int num = 0;

    for (;;)
    {
        num = va_arg(args, int);
        if(num != -1){
            sum += num;
        }else{
            break;
        }
    }
    va_end(args);
    return sum;
}

C/S通信模型

UDP通信模型
graph TB
socket-->bind
bind-->recvfrom
recvfrom-->阻塞等待客户数据
阻塞等待客户数据-->处理请求
处理请求-->sendto
sendto-->close

socket1-->sendto1
sendto1-->recvfrom1
recvfrom1-->close1

sendto1-.服务请求.->阻塞等待客户数据
sendto-.服务应答.->recvfrom1
TCP模型
graph TB
socket-->bind
bind-->listen
listen-->accept
accept-->阻塞等待客户数据
阻塞等待客户数据-->read
read-->处理请求
处理请求-->write
write-->close

socket1-->connect
connect-->write1
write1-->read1
read1-->close1

connect-.建立连接.->阻塞等待客户数据
write1-.请求数据.->read
write-.应答数据.->read1
UDP循环服务器
  1. 调用socket()函数创建UDP套接字
  2. 调用bind()函数将套接字绑定到本地可用端点地址
  3. while(1)死循环
  4. 循环体内
    1. 调用recvfrom()函数读取客户请求
    2. 处理数据
    3. 调用sendto()函数返回数据给客户
TCP循环服务器
  1. 调用socket()函数创建TCP套接字
  2. 调用bind()函数将套接字绑定到本地可用端点地址
  3. 调用listen()函数将套接字设为被动模式
  4. while(1)死循环
    1. 调用accept()函数接收客户请求并建立一个处理该连接的临时套接字
    2. 调用recv/send()函数进行数据相互传递
    3. 交互完毕,关闭临时套接字(accept返回的套接字)

linux下的服务器并发机制

进程相关

fork函数

linux下用于创建进程。失败返回-1,成功在父进程中返回子进程的实际pid,在子进程中返回0。

#include<unistd.h>
int fork();

示例

#include<unistd.h>

int main()
{
    int a = 0;
    pid_t p;
    
    p = fork();
    if(p == -1){
        fprintf(stderr,"创建进程失败\n");
    }else{
        if(p == 0){
            a += 2;
            fprintf(stdout,"子进程:%d",a);
        }else{
            a += 4;
            fprintf(stdout,"父进程:%d",a);
        }
    }
    return 0;
}
getpid函数

获取当前进程的id号

getppid函数

获取当前进程的父进程id号

#include<unistd.h>

pid_t getpid(void);
pid_t getppid(void);

僵尸进程的避免
  1. 父进程调用wait()或waitpid()等函数等待子进程结束,但是这会使父进程挂起(进入阻塞或等待状态)
  2. 如果父进程很忙不能被挂起,可以调用signal()函数为SIGCHLD信号安装handler来避免。当子进程结束后,内核发送SIGCHLD信号给其父进程,父进程收到信号后则可在handler中调用wait()函数来进行回收。
  3. 如果父进程不关心子进程何时结束,则可调用signal(SIGCHLD,SIG_IGN)函数通知内核,让子进程在结束时由内核自动回收,且内核不会给父进程发送SIGCHLD信号。
  4. 当父进程结束后,子进程就成了孤儿进程。从而会被过继给1号进程init。init进程主要负责系统启动服务以及子进程的清理回收。所以当过继给init进程的子进程结束后也会自动回收。
wait()函数

进程一旦调用wait()函数就会立即阻塞自己。由wait()函数自动分析当前进程的某个子进程已经退出,如果找到这样一个已经变成僵尸的子进程,waith()函数就会收集该子进程的信息并进行回收,如果没有找到这样的子进程,那么就会一直阻塞,直到出现为止。调用成功返回被收集的子进程的进程ID,失败返回-1

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int * status)
status:用来保存子进程退出时的状态。可以通过调用一下宏来判断子进程结束状况:
    WIFEXITED(status):子进程正常结束该宏返回非0值
    WEXITSTATUS(status):若子进程正常结束,用这个宏可以获得子进程由exit()返回的结束代码
    WIFSIGNALED(status):子进程因为信号而结束则该宏返回非0值
    WTERMSIG(status):若子进程因为信号结束则该宏可以获得子进程中止信号代码
    WIFSTOPPEN(status):子进程处于暂停执行状态则返回非0值
    WSTOPSIG(status):若子进程处于暂停执行状态怎该宏获得引发暂停状态的信号代码
waitpid()函数

waitpid()会像wait()一样阻塞父进程直到子进程退出。waitpid()正常的时候返回子进程pid,如果设置了WNHAND选项,如果调用waitpid时没有子进程可收集,那么返回0。出错返回-1

#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status, int options)
pid:需要等待的那个子进程号
    pid>0:只有等待到的子进程号等于pid时,waitpid()才停止阻塞父进程。
    pid=-1:等待任何一个子进程退出就停止阻塞。此时等价wait()
    pid=0:等待同一组中的任何子进程。
    pid<-1:等待指定组中的任何一个子进程,组id为与pid的绝对值。
status:被收集的子进程退出状态
options:主要含有以下参数,多个参数可以相与。
    WNOHANG:子进程没有退出,waitpid也立即返回
    WUNTRACED:当子进程处于暂停状态waitpid立即返回
    0:相当于不设特殊参数
signal()函数

用于绑定收到指定信号的处理函数

#include<signal.h>
void (* signal(int signum,void (*handler)(int)))(int) 
signum:信号编号
handler:
    void (*)(int)类型的函数名,当收到signum信号时执行handler函数
    SIG_IGN:忽略信号
    SIG_DFL:恢复成系统信号的默认处理

线程相关

gcc编译时需要连接上线程库 gcc -lpthread xxx.c -o xxx

pthread_create()函数

创建一个新线程,成功返回0,失败返回非0。

#include<pthread.h>
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(* start_routine)(void *), void *arg)
thread:创建的线程标识符
attr:线程运行属性的结构体
start_routine:参数类型时void *返回值也是void *的函数指针。这个是线程执行的函数体。
arg:传递给线程体函数的参数

typedef struct{
    int detachstate; //分离状态
    int schedpolicy; //线程调度策略
    struct sched_param schedparam; //线程调度参数
    int inheritsched; //线程继承性
    int scope; //线程作用域
    size_t guardsize;//线程堆栈保护区大小
    int stackaddr_set;//线程堆栈地址集
    void * stackaddr;//线程堆栈地址
    size_t stacksize;//线程堆栈大小
}pthread_attr_t;

LINUX下可以通过下面这些函数设置线程的运行状态。

#include<pthread.h>
int pthread_attr_init(pthread_attr_t * attr)//初始化结构体
int pthread_attr_destroy(pthread_attr_t * attr)//去初始化结构体

//设置/获取线程分离状态
int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate)
int pthread_attr_setdetachstate(const pthread_attr_t *attr,int detachstate)
//成功返回0,失败返回-1
/***
detachstate:
    PTHREAD_CREATE_DETACHED:以分离状态运行
    PTHREAD_CREATE_JOINABLE:非分离状态运行(默认)
*/
其他的set,get函数类似
pthread_exit()函数

终止一个进程

#include<pthread.h>
int pthread_exit(void * value_ptr)

int pthread_join(pthread_t thread, void ** value_ptr)
exit中的value_ptr:线程返回值指针,该返回值将被传递给另一个线程,可以通过调用pthread_join()函数获取

thread:等待结束的进程标识符
join中value_ptr:如果不为NULL,那么线程thread的返回值将存储在指针指向的位置。
pthread_self()函数

获取线程标识符

#include<pthread.h>
pthread_t phread_self()
pthread_detach()函数

将线程设置成分离线程。成功返回0,失败返回错误号

int pthread_detach(pthread_t thread)

不固定数量的进程并发模型

  1. 主进程创建套接字msock并绑定到熟知端口
  2. 主进程调用accept()函数等待客户连接的到达
  3. 当有客户连接时,主进程建立与客户端的通信连接,同时accept()函数返回新的套接字ssock
  4. 主进程创建新的从进程来处理ssock
  5. 主进程调用close()函数将ssock的引用计数减一
  6. 主进程返回步骤2
  7. 从进程关闭msock(close()函数,减少引用计数,shutdown()函数才是直接关闭套接字的)
  8. 从进程调用recv/send函数进行客户端的数据处理
  9. 从进程处理完毕,关闭ssock,结束进程。

固定进程数的并发模型

  1. 父进程
    1. 主进程创建主套接字msock,并绑定到熟知端口
    2. 主进程创建给定的数量的从进程
    3. 主进程调用wait()函数等待从进程结束,一旦有从进程退出,则调用fork()创建新的进程,以保持数量不变
  2. 从进程
    1. 从进程调用accept()等待客户连接到达
    2. 当有客户连接时,从进程建立与客户端的通信连接,同时accept()函数返回新的套接字ssock
    3. 从进程调用recv/send函数处理
    4. 处理完毕,从进程关闭套接字

互斥锁

互斥锁就是排他锁,一个时间只有一个线程能拥有该锁

互斥锁有三种:快速互斥锁,递归互斥锁,检错互斥锁。
快速互斥锁:调用的线程会阻塞直到解锁为止。
递归互斥锁:能成功的返回并增加调用线程在互斥锁上的加锁次数
检错互斥锁:调用的线程不会被阻塞,它会立刻返回一个错误信息

操作步骤

graph TB
定义互斥锁变量pthread_mutex_t-->定义互斥锁属性pthread_mutexattr_t
定义互斥锁属性pthread_mutexattr_t-->初始化互斥锁属性变量pthread_mutexattr_init
初始化互斥锁属性变量pthread_mutexattr_init-->设置互斥锁属性pthread_mutexattr_setXXX
设置互斥锁属性pthread_mutexattr_setXXX-->初始化互斥锁变量pthread_mutex_init
初始化互斥锁变量pthread_mutex_init-->互斥锁上锁pthread_mutex_lock
互斥锁上锁pthread_mutex_lock-->互斥锁判断上锁pthread_mutex_trylock
互斥锁判断上锁pthread_mutex_trylock-->互斥锁解锁pthread_mutex_unlock
互斥锁解锁pthread_mutex_unlock-->消除互斥锁pthread_mutex_destroy

相关函数原型

#include<pthread.h>
/**
初始化互斥锁变量
*/
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutexattr)
/**
初始化互斥锁属性变量
*/
int pthread_mutexattr_init(pthread_mutexattr_t *mattr)
/**
释放互斥锁属性变量
*/
int pthread_mutexattr_destroy(pthread_mutexattr_t *mattr)

/**
成功返回0,失败返回错误编号
*/
int pthread_mutex_lock(pthread_mutex_t *mutex)

/**
在互斥锁被其他线程锁住时立刻返回,不会阻塞当前进程。其他情况作用和pthread_mutex_lock()函数一样
*/
int pthread_mutex_trylock(pthread_mutex_t * mutex)
/***
解锁互斥锁
*/
int pthread_mutex_unlock(pthread_mutex_t * mutex)
/***
释放互斥锁变量。成功返回0,失败返回错误编号
*/
int pthread_mutex_destroy(pthread_mutex_t * mutex)
/**
互斥锁的共享属性
*/
int pthread_mutexattr_setshared(const pthread_attr_t *mattr, int pshared)
int pthread_mutexattr_getshared(const pthread_attr_t *mattr, int * pshared)
//pshared : PTHREAD_PROCESS_PRIVATE 和 PTHREAD_PROCESS_SHARED

/**
互斥锁类型
*/
int pthread_mutexattr_settype(pthread_mutexattr_t *mattr, int type)
int pthread_mutexattr_gettype(pthread_mutexattr_t *mattr, int *type)
//type: PTHREAD_MUTEX_NOMAL 快速互斥锁 PTHREAD_MUTEX_RECURSIVE 递归互斥锁 PTHREAD_MUTEX_ERRORCHECK 检错互斥锁

信号量

有名信号量,保存在文件中可以多线程同步和多进程同步
无名信号量,保存在内存,用于同一进程的不同线程同步。

无名信号量

sem_init()函数

初始化信号量。成功返回0,失败返回-1

#include<semaphore.h>
int sem_init(sem_t * sem,int pshared, unsigned int value)
sem:实质是一个int类型指针
pshared:决定能否在几个进程间共享。为0代表只能本进程的线程共享。不为0代表在多个进程间共享
value:初始化信号量的值
sem_wait()函数

阻塞当前线程直到信号量sem大于0。成功返回0,sem减1,出错返回-1

#include<semaphore.h>
int sem_wait(sem_t *sem)
sem_trywait()

sem_wait()的非阻塞版本,成功返回0,sem减1,失败立即返回-1;

#include<semaphore.h>
int sem_trywait(sem_t * sem)
sem_post()函数

回退资源。成功返回0,将sem加1;出错返回-1;

#include<semaphore.h>
int sem_post(sem_t *sem)
sem_getvalue()

用于获取信号量数量。成功返回0,出错返回-1.

#include<semaphore.h>
int sem_getvalue(sem_t *sem,int *sval)
sem_destroy()函数

归还信号量所占的资源。成功返回0,出错返回-1

#include<semaphore.h>
int sem_destroy(sem_t *sem)

有名信号量

有名信号量和无名信号量共用sem_wait(),sem_trywait(),sem_post()函数
有名信号量用sem_open()函数初始化,结束时需要调用sem_close()和sem_unlink()函数(有名信号量使用的是文件存储,所以需要关闭还要删除)

sem_open()函数

创建或打开已存在的有名信号量。成功返回信号量指针,出错返回SEM_FAILED

#include<semaphore.h>
sem_t * sem_open(const char* name,int oflag, mode_t mode,unsigned int value)
name:信号量的外部名称。信号量创建的文件都在/dev/shm目录下,指定名字时不能包含路径。
oflag:O_CREATE信号量不存在时则创建,且此时mode和value必须有效;若存在时则打开信号量,自动忽略mode和value。O_CREATE | EXCL若信号量不存在则和O_CREATE时一样,若存在将返回一个错误
mode:同文件权限
value:信号量初始值
sem_close()、sem_unlink()函数

sem_close函数用于关闭信号量,并释放资源。sem_unlink函数用于信号量关闭后删除所值的信号量。成功返回0,出错返回-1

#include<semaphore.h>
int sem_close(sem_t *sem)
int sem_unlink(const char *name)

条件变量

条件变量是一种同步机制,允许线程挂起,直到共享数据上某些条件得到满足。条件变量是利用线程间的全局变量进行同步的一种机制。一个线程等待条件变量的条件成立,另一个线程使条件成立并发出条件成立信号。
条件变量一般要和互斥锁一起使用,基本操作步骤:

  1. 声明pthread_cond_t条件变量,使用pthread_cond_init()函数初始化
  2. 声明pthread_mutex_t变量,调用pthread_mutex_init()函数初始化
  3. 调用pthread_cond_signal()函数发出信号,如果此时有线程在等待该信号,则线程被唤醒,否则忽略该信号。如果想让所有等待该信号的线程都唤醒,则调用pthread_cond_broadcast()函数
  4. 调用pthread_cond_wait()/pthread_cond_timedwait()等待信号。如果没有信号就阻塞。在调用之前必须先获得互斥量,如果线程阻塞则释放互斥量
  5. 调用pthread_cond_destroy()函数销毁条件变量,释放所占的资源。

相关函数原型:

#include<pthread.h>
/***
attr:赋值为NULL,还没有定义相关结构体
*/
int pthread_cond_init(pthread_cond_t * cond,const pthread_condattr_t *attr)

/****
发出条件变量,表示满足条件成立。唤醒一个线程
*/
int pthread_cond_signal(pthread_cond_t *cond)
/***
唤醒所有等待cond信号条件的线程
*/
int pthread_cond_broadcast(pthread_cond_t *cond)
/***
等待条件满足,成功返回0,出错返回错误编号
*/
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
/***
计时等待,在给定时间内等待,如果还是不满足则返回ETIMEOUT,否则成功返回0,失败返回错误编号。
abstime:和time()函数返回值代表的意义相同。GTM时间
*/
int pthread_cond_timedwait(pthread_cond_t * cond,pthread_mutex_t * mutex, const struct timespec * abstime)
/***
销毁指定条件变量,成功返回0,失败返回错误编号
*/
int pthread_cond_destroy(pthread_cond_t *cond)

示例程序:

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void * thread1(void *);
void *thread2(void *);
int i = 1;
int main()
{
    pthread_t t_a;
    pthread_t t_b;
    pthread_create(&t_a,NULL,thread1,(void *)NULL);
    pthread_create(&t_b,NULL,thread2,(void *)NULL);
    pthread_join(t_a,NULL);
    pthread_join(t_b,NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}
void * thread1(void *arg)
{
    for(i = 1;i <= 9 ;i++){
        pthread_mutex_lock(&mutex);
        if(i % 3 == 0){
            pthread_cond_signal(&cond);
        }else{
            printf("thread 1:%d\n",i);
        }
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}
void * thread2(void *arg)
{
    while(i < 9){
        pthread_mutex_lock(&mutex);
        if(i % 3 != 0){
            pthread_cond_wait(&cond,&mutex);
        }
        printf("thread2:%d\n",i);
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}
结果:
thread1:1
thread1:2
thread2:3
thread1:4
thread1:5
thread2:6
thread2:6
thread1:7
thread1:8
thread2:9

基于单线程的并发服务器

SELECT事件驱动模型 (windows,linux)
select()函数

提供异步io。让单线程/进程等待指定集合中任意一个文件描述符就绪。当没有设备准备就绪时,select()函数阻塞,若集合中任意一个设备准备就绪,select()函数返回。正常情况下select()返回就绪的文件描述符个数,如果timeout时间后还没有就绪的,那么返回0。如果select()被某个信号中断,返回-1,。调用出错返回-1。

#include<pthread.h>
int select(int maxfdp, fd_set *readfds, fd_set* writefds, fd_set* errorfds, struct timeval * timeout)
maxfdp:指集合中所有文件描述符的范围。即所有文件描述符的最大值加1,该参数通常使用getdtablesize()函数获得
readfds:fd_set结构指针。存放可读文件描述符集合。如果集合中的文件有一个可读,那么select()就会返回大于0的值,如果没有就会根据timeout的值判断超时,超时返回0,出错返回-1
writefds:存放可写文件描述符集合。类似readfds。
errorfds:用来监控文件错误异常
timeout:timeval结构体指针,取值如下
    NULL:select()处于阻塞模式,且永不超时
    0秒0毫秒:select()函数处于非阻塞模式
    大于0:设定定时器

fd_set结构体的操作宏
    FD_ZERO(&fdset);//将fdset清零,清空fdset和文件句柄之间的关系
    FD_SET(fd,&fdset);//将fd加入fdset
    FD_CLR(fd,&fdset);//将fd从fdset中删除
    FD_ISSET(fd,&fdset);//判断fd是否在fdset中就绪

struct timeval{
    long tv_sec;//秒
    long tv_usec;//毫秒
};

示例程序片段:

...
#include<pthread.h>

int main()
{
    ...
    fd_set rfds;//可读集合
    fd_set afds;//保存所有的文件描述符集合
    
    msock = socket(...);
    bind...
    listen...
    
    int nfds = getdtablesize();//获取最大描述符个数
    FD_ZERO(&rfds);
    FD_SET(msock,&afds);//将msock加入可读集合
    ...
    while(1){//死循环
        memcpy(&rfds,&afds,sizeof(rfds)); 
        if(select(nfds,&rfds,(fd_set*)0,(fd_set*)0,(struct timeval *)0) < 0)
        {
            errexit("select 出错:%s\n",strerror(errno));
        }
        if(FD_ISSET(msock,rfds))
        {
            ...
            ssock = accept(...)
            if(ssock < 0){
                 errexit("accept 出错:%s\n",strerror(errno));
            }
            FD_SET(ssock,rfds);
        }
        for(fd = 0;fd < nfds,++fd){
            if(fd != msock && FD_ISSET(fd,rfds))
            {
                ...
                recv(...)
                ...
                send(...)
                close(fd);
                FD_CLR(fd,&afds);
            }
        }
    }
}

线程池

预先创建线程,在线程不足的情况下再创建新的。适用于线程任务时间短需要的线程多的情况,避免大量的时间浪费在创建线程上。

风险:同步错误,死锁,池的死锁,资源不足,线程泄漏等。

基于Epoll模型的并发 (linux)

相对于select模型,epoll模型具有更大的文件描述符数量。select由FD_SETSIZE设置,其默认大小为2048,要修改只能修改后重新编译内核,而epoll则支持的数量远远大于该值,其最大值跟内存大小有关系,可以通过 cat /proc/sys/fs/file -max 查看。其次是选择就绪的文件的方式,select采用的是遍历的方式,epoll采用的是基于事件的选择。选择效率比select的遍历方式高。

相关API
epoll_create()函数

创建epoll句柄。返回一个文件描述符,记得用完之后使用close()函数关闭,否则可能导致文件描述符耗尽。

#inlcude<sys/epoll.h>
int epoll_create(int size);
size:用来告诉内核监听的数目一共有多大。
epoll_ctl()函数

为epoll的事件注册函数

#include<sys/epoll.h>
int epoll_ctl(int epfd,int op, int fd, struct epoll_event * event)
epfd:epoll_create()函数返回的句柄。
op:
    EPOLL_CTL_ADD:注册新的fd到epfd中
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件
    EPOLL_CTL_DEL:从epfd中删除fd
event:告诉内核需要监听什么事件。
    struct epoll_event{
        __uint32_t events;//事件
        epoll_data_t data;//用户变量数据
    };
    events可以是一下宏的集合:
        EPOLLIN:对应文件描述符可以读(包括对端socket正常关闭)
        EPOLLOUT:对应文件描述符可以写
        EPOLLPRI:对应文件描述符有紧急数据可读
        EPOLLERR:对应文件描述符发生错误
        EPOLLHUP:对应文件描述符被挂起
        EPOLLET:将epoll设为边缘触发模式
        EPOLLONESHOT:只监听一次事件,监听完后如果还想再次监听,则需要再次加入epoll队列
epoll_wait()函数

等待事件产生。返回需要处理事件的数目,返回0表示已经超时。

#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
events:用来从内核得到事件集合
maxevents:告诉内核events有多大。不能大于epoll_create()函数传入的size的大小。
timeout:超时时间(毫秒)。0 立即返回,-1 永久阻塞
触发模式

水平触发(LT),边缘触发(ET)
ET模式事件效率高,但是编程复杂,需要程序员仔细处理事件,否则容易漏事件。
LT模式效率比ET模式低,但编程容易。

示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include<unistd.h>
#include<arpa/inet.h>
//#include<openssl/ssl.h>
//#include<openssl/err.h>
#include<fcntl.h>
#include<sys/epoll.h>
#include<sys/time.h>
#include<sys/resource.h>

#define MAXBUF 1024
#define MAXEPOLLSIZE 10000

int setnonblocking(int sockfd)
{
    if(fcntl(sockfd,F_SETFL,fcntl(sockfd,F_GETFD,0)|O_NONBLOCK) == -1){
        return -1;
    }
    return 0;
}
int handle_message(int new_fd){
    char buf[MAXBUF+1];
    int len;
    bzero(bug, MAXBUF+1);
    len = recv(new_fd, buf,MAXBUF,0);
    if(len > 0){
        printf("%d接收消息成功:'%s',共%d字节的数据",new_fd,buf,len);
    }else{
        if(len < 0){
            printf("接受消息失败!错误代码:%d,错误信息:%s\n",errno,strerror(errno));
        }
        close(new_fd);
        return -1;
    }
    return len;
}
int main(int argc,char *argv[])
{
    int listener,new_fd,kdpfd,nfds,n,ret,curfds;
    socklen_t len;
    struct sockaddr_in my_addr,their_addr;
    unsigned int myport,lisnum;
    struct epoll_event ev;
    struct epoll_event events[MAXEPOLLSIZE];
    struct rlimit rt;
    myport = 5000;
    lisnum = 2;
    
    rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;
    if(setrlimit(RLIMIT_NOFILE,&rt) == -1){
        perror("setrlimit");
        exit(1);
    }else{
        printf("设置系统参数成功!\n");
    }
    if((listener = socket(PF_INET,SOCK_STREAM,0))== -1){
        perror("socket");
        exit(1);
    }else{
        printf("socket创建成功!");
    }
    setnonblocking(listener);
    bzero(&my_addr,sizeof(my_addr));
    my_addr.sin_family = PF_INET;
    my_addr.sin_port = htons(myport);
    my_addr.sin_addr.s_addr = INADDR_ANY;
    if(bind(listener,(struct sockaddr *)&my_addr,sizeof(struct sockaddr)) == -1){
        perror("bind");
        exit(1);
    }else{
        printf("ip地址端口绑定成功\n");
    }
    if(listen(listener,lisnum) == -1){
        perror("listen");
        exit(1);
    }else{
        printf("开启服务成功\n");
    }
    kdpfd = epoll_create(MAXEPOLLSIZE);
    len = sizeof(struct sockaddr_in);
    ev.events = EPOLLIN|EPOLLET;
    ev.data.fd = listener;
    if(epoll_ctl(kdpfd,EPOLL_CTL_ADD,listener,&ev)< 0){
        fprintf(stderr,"epoll set insertion error:fd = %d\n",listener);
        return -1;
    }else{
        printf("监听socket加入epoll成功\n");
    }
    curfds = 1;
    while(1){
        nfds = epoll_wait(kdpfd,events,curfds,-1);
        if(nfds == -1){
            perror("epoll_wait");
            break;
        }
        for(n = 0;n < nfds;n++){
            if(events[n].data.fd == listerner){
                new_fd = accept(listener,(struct sockaddr *)&their_addr,&len);
                if(new_fd < 0){
                    perror("accept");
                    continue;
                }else{
                   printf("有连接来自:%d:%d,分配的socket为:%d\n",inet_ntoa(their_addr.sin_addr),ntohs(their_addr.sin_port),new_fd); 
                }
                setnonblocking(new_fd);
                ev.events = EPOLLIN|EPOLLET;
                ev.data.fd = new_fd;
                if(epoll_ctl(kdpfd,EPOLL_CTL_ADD,new_fd,&ev) < 0){
                    printf(stderr,"吧socket %d 加入epoll失败! %s",new_fd,strerror(errno));
                    return -1;
                }
                curfds++;
            }else{
                ret = handle_message(events[n].data.fd);
                if(ret < 1 && errno != 11){
                    epoll_ctl(kdpfd,EPOLL_CTL_DEL,events[n].data.fd,&ev);
                    curfds--;
                }
            }
        }
    }
    close(listener);
    return 0;
}

死锁

原因
  1. 竞争资源
  2. 进程推进顺序不当
产生死锁的必要条件
  1. 互斥条件:存在独占资源。
  2. 请求和保持条件:占有资源后提出新的资源请求,没有得到满足但是又不释放自己已经获得的资源。
  3. 不剥夺条件:进程已经获得的资源在未使用完之前不能被剥夺。
  4. 环路等待条件:指进程的资源需求形成了环形。A->B->C->A

存在死锁的示例程序:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<ctype.h>
#include<pthread.h>
#define LOOP_TIMES 10000

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

void *thread_worker(void *);
void critical_sesstion(int thread_num,int i);
int main(void)
{
    int rtn,i;
    pthread_t pthread_id = 0;
    rtn = pthread_create(&pthread_id,NULL,thread_worker,NULL);
    if(rtn != 0){
        printf("pthread_create error!\n");
        return -1;
    }
    for(i = 0;i < LOOP_TIMES;i++){
        pthread_mutex_lock(&mutex1);
        pthread_mutex_lock(&mutex2);
        critical_section(1,i);
        pthread_mutex_unlock(&mutex2);
        pthread_mutex_unlock(&mutex1);
    }
    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);
    return 0;
}
void * thread_worker(void * p)
{
    int i = 0;
    for(i = 0;i<LOOP_TIMES;i++){
        pthread_mutex_lock(&mutex2);
        pthread_mutex_lock(&mutex1);
        critical_section(2,i);
        pthread_mutex_unlock(&mutex1);
        pthread_mutex_unlock(&mutex2);
    }
}
void critical_section(int thread_num,int i)
{
    printf("thread%d:%d\n",thread_num,i);
}
相关标签: TCP/IP