本文学习于:C语言技术网(www.freecplus.net),并加以自己的一些理解和复现,如有侵权会删除。
现在把 C++ 基础知识,算法,也学习完成基础计算机网络知识,同时也对操作系统,数据结构进行了简要的学习。
学习网络编程,跟着视频 B 站 UP 主 C 语言技术网,《C/C++ 网络编程,从 socket 到 epoll 》视频学习,同时也根据这个网址进行学习,网址如下:http://www.freecplus.net/44e059cca66042f0a1286eb188c51480.html,我只是为了学习记录,如果有侵权立刻删除,里面程序没有做任何改动,自己理解部分会在下面标注。听说网络编程 C/C++ 是最难掌握的技术,要求掌握信号、多进程、多线程知识,今天就来学习,然后理解它。这个系列视频从最基础的 socket 讲起,然后是多进程/多线程网络服务程序开发,到 I/O 复用(select、poll 和 epoll) 知识。
在学习项目过程中,仔细对了里面源码进行学习,对基础知识不理解,不系统,一点一点的查阅,效率相当慢,而且不能理解整个架构。因此,静下心来把这个课程过一遍,实践中学习。
socket 就是套接字,运行在计算机中的两个程序通过 socket 建立起一个通道,数据在通道中传输。socket 把复杂的 TCP/IP 协议族隐藏起来了,只要用好 socket 相关函数,就可以完成网络通信。
socket 提供了流(stream)和数据包(datagram)两种通信机制。stream socket 基于 TCP 协议,是一个有序,可靠,双向字节流的通道,传输数据不会丢失、不重复,顺序也不会乱。datagram socket 是基于 UDP 协议,不需要建立和维持连接,可能会丢失或者错乱。
简单的 socket 通讯流程,
客户端:
socket() -> connect() -> send()/recv() -> close()
1. 创建流式 socket
2. 向服务器发起连接请求
3. 发送/接收数据
4. 关闭 socket 连接,释放资源
服务端
socket() -> bind() -> listen() -> accept() -> recv()/send() -> close()
1. 创建流式 socket
2. 指定通信的 ip 地址和端口
3. 把 socket 设置为监听模式
4. 接受客户端连接
5. 接受/发送数据
6.关闭 socket 连接,释放资源
下面学习一个服务器和客户通信的程序,但是程序执行之前,需要满足在运行程序之前,必须保证服务器的防火墙已经开通了网络访问策略(云服务器还需要登录云控制平台开通访问策略)。 ---- 因此需要安装防火墙,还有打开防火墙,打开端口。
安装设置防火墙步骤 :
/*安装防火墙部分*/
// 1. 开启管理员模式
su
// 2. 安装防火墙
yum install firewall
// 3.
systemctl start firewalld
// 4. 开启防火墙
systemctl enable firewalld
// 5. 设置防火墙状态,如果进入文档编辑页面,按住 : ,然后输入 q 就退出了
systemctl status firewalld
/*设置开启端口,查看状态部分*/
// 1. 查看防火墙工作状态
firewall-cmd --state
// 2. 开启 5000 tcp 端口
firewall-cmd --zone=public --add-port=5000/tcp --permanent
// 3. 重新加载,不然显示不出来端口号
firewall-cmd --reload
// 4. 查看开启端口号
firewall-cmd --list-port
运行如下:
在配置好防火墙以后,开始运行客户端和服务器的程序源码。首先注意,在 linux 创建一个 cpp 文件如下
touch server.cpp
touch client.cpp
// 创建 makefile
touch makefile
然后查看自己虚拟机 ip 地址的方式为:
ifconfig -a
在这里 192.168.201.129 就是我的 ip 地址。
然后打开 server.cpp 文件,代码如下,
/*
* 程序名:server.cpp,此程序用于演示socket通信的服务端
* 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
if (argc!=2)
{
printf("Using:./server port\nExample:./server 5005\n\n"); return -1;
}
// 第1步:创建服务端的socket。
int listenfd;
if ( (listenfd = socket(AF_INET,SOCK_STREAM,0))==-1) {
perror("socket"); return -1; }
// 第2步:把服务端用于通信的地址和端口绑定到socket上。
struct sockaddr_in servaddr; // 服务端地址信息的数据结构。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET。
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。
//servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 指定ip地址。
servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口。
if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
{
perror("bind"); close(listenfd); return -1; }
// 第3步:把socket设置为监听模式。
if (listen(listenfd,5) != 0 ) {
perror("listen"); close(listenfd); return -1; }
// 第4步:接受客户端的连接。
int clientfd; // 客户端的socket。
int socklen=sizeof(struct sockaddr_in); // struct sockaddr_in的大小
struct sockaddr_in clientaddr; // 客户端的地址信息。
clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen);
printf("客户端(%s)已连接。\n",inet_ntoa(clientaddr.sin_addr));
// 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
char buffer[1024];
while (1)
{
int iret;
memset(buffer,0,sizeof(buffer));
if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) // 接收客户端的请求报文。
{
printf("iret=%d\n",iret); break;
}
printf("接收:%s\n",buffer);
strcpy(buffer,"ok");
if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) // 向客户端发送响应结果。
{
perror("send"); break; }
printf("发送:%s\n",buffer);
}
// 第6步:关闭socket,释放资源。
close(listenfd); close(clientfd);
}
然后打开 client.cpp 文件
/*
* 程序名:client.cpp,此程序用于演示socket的客户端
* 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
if (argc!=3)
{
printf("Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n"); return -1;
}
// 第1步:创建客户端的socket。
int sockfd;
if ( (sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) {
perror("socket"); return -1; }
// 第2步:向服务器发起连接请求。
struct hostent* h;
if ( (h = gethostbyname(argv[1])) == 0 ) // 指定服务端的ip地址。
{
printf("gethostbyname failed.\n"); close(sockfd); return -1; }
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0) // 向服务端发起连接清求。
{
perror("connect"); close(sockfd); return -1; }
char buffer[1024];
// 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
for (int ii=0;ii<3;ii++)
{
int iret;
memset(buffer,0,sizeof(buffer));
sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1);
if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0) // 向服务端发送请求报文。
{
perror("send"); break; }
printf("发送:%s\n",buffer);
memset(buffer,0,sizeof(buffer));
if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0) // 接收服务端的回应报文。
{
printf("iret=%d\n",iret); break;
}
printf("接收:%s\n",buffer);
}
// 第4步:关闭socket,释放资源。
close(sockfd);
}
接着打开 makefile 文件,输入
all:client server
client:client.cpp
g++ -g -o client client.cpp
server:server.cpp
g++ -g -o server server.cpp
最后就到了运行他们的环节,首先在 linux 开启两个终端,
// 1. 选择一个终端,先进行编译,使用 make 指令
make
/*终端 1*/
// 对防火墙进行检测和开启
systemctl start firewalld
systemctl enable firewalld
firewall-cmd --state
firewall-cmd --zone=public --add-port=5000/tcp --permanent
firewall-cmd --reload
firewall-cmd --list-port
// 运行客户端程序,用 tcp 5000 端口号
./client 127.0.0.1 5000
/*终端 2*/
// 运行服务器程序,用 tcp 5000 端口号
./server 5000
结果如下
对程序进行学习,里面基本都是固定格式,需要修改的很少,所以就自己看和理解了。因为里面很多是库文件宏定义或者库函数,因此先记固定格式,熟悉了以后慢慢修改尝试应用。里面提到的我就按照上面学习了,其网址如下:
http://www.freecplus.net/0047ac4059b14d52bcc1d4df6ae8bb83.html
现在开始自己的分析了,其实刚开始我是存在疑问的,为什么任意 ip 都可以通信,后来看了源代码是这一句,
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。
//servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 指定ip地址。
然后记录觉得重要的几点,
1. socket()函数的返回值其本质是一个文件描述符,是一个整数。
2. 两个重要的发送和接收函数
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd 为建立的 socket,buf 为发送和接收的缓存,flags = 0
函数返回已发送的字符数。出错时返回-1,错误信息errno被标记。
3. 对服务端来说,有两个socket,一个是用于监听的socket,还有一个就是客户端连接成功
后,由accept函数创建的用于与客户端收发报文的socket。
4. 申请 socket 资源
int socket(int domain, int type, int protocol);
domain 协议族,宏定义; type 指定类型,宏定义; protocol 传输协议方式
返回值:成功则返回一个socket,失败返回-1,错误原因存于errno 中。
第一个参数只能填AF_INET,第二个参数只能填SOCK_STREAM,第三个参数只能填0。
5. 把ip地址或域名转换为hostent 结构体表达的地址。
struct hostent *gethostbyname(const char *name);
name:域名或者主机名;
返回值:如果成功,返回一个hostent结构指针,失败返回NULL。
gethostbyname只用于客户端。
6. 向服务器发起连接请求。
int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);
函数说明:connect函数用于将参数sockfd 的socket 连至参数serv_addr 指定的服务
端,参数addrlen为sockaddr的结构长度。
返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
connect函数只用于客户端。
7. 服务端把用于通信的地址和端口绑定到socket上。
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数sockfd,需要绑定的socket。
参数addr,存放了服务端用于通信的地址和端口。
参数addrlen表示addr结构体的大小。
返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
8. listen函数把主动连接socket变为被动连接的socket,使得这个socket可以接受其它
socket的连接请求,从而成为一个服务端的socket。
int listen(int sockfd, int backlog);
参数sockfd是已经被bind过的socket。
参数backlog,这个参数涉及到一些网络的细节,比较麻烦,填5、10都行,一般不超过30。
当调用listen之后,服务端的socket就可以调用accept来接受客户端的连接请求。
返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
9. 服务端接受客户端的连接。
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
参数sockfd是已经被listen过的socket。
参数addr用于存放客户端的地址信息,用sockaddr结构体表达,如果不需要客户端的地址,可
以填0。
参数addrlen用于存放addr参数的长度,如果addr为0,addrlen也填0。
accept函数等待客户端的连接,如果没有客户端连上来,它就一直等待,这种方式称之为阻
塞。
对于两个结构体定义
struct sockaddr_in{
short sin_family;/*Address family一般来说AF_INET(地址族)PF_INET(协议族)*/
unsigned short sin_port;/*Port number(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)*/
struct in_addr sin_addr;/*IP address in network byte order(Internet address)*/
unsigned char sin_zero[8];/*Same size as struct sockaddr没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐*/
};
/*该结构记录主机的信息,包括主机名、别名、地址类型、地址长度和地址列表。*/
struct hostent{
char * h_name;/*地址的正式名称*/
char ** h_aliases;/* 空字节-地址的预备名称的指针*/
short h_addrtype;/*地址类型; 通常是AF_INET*/
short h_length;
char ** h_addr_list;
#define h_addr h_addr_list[0];
};
char *h_name 表示的是主机的规范名。例如 www.google.com 的规范名其实是 www.l.google.com
char **h_aliases 表示的是主机的别名。 www.google.com 就是google他自己的别名。有的时候,有的主机可能有好几个别名,这些,其实都是为了易于用户记忆而为自己的网站多取的名字。
int h_addrtype 表示的是主机ip地址的类型,到底是ipv4(AF_INET),还是ipv6(AF_INET6)
int h_length 表示的是主机ip地址的长度
int **h_addr_lisst 表示的是主机的ip地址,注意,这个是以网络字节序存储的。千万不要直接用printf带%s参数来打这个东西,会有问题。所以到真正需要打印出这个IP的话,需要调用inet_ntop()。
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt) :
这个函数,是将类型为af的网络地址结构src,转换成主机序的字符串形式,存放在长度为cnt的字符串中。
这个函数,其实就是返回指向dst的一个指针。如果函数调用错误,返回值是NULL。
注意一点,对于main 函数中两个参数的意义,可以参考下面的网址进行学习:https://blog.csdn.net/sun1314_/article/details/71271641。
通过对源代码学习理解以后,结构体可以自己百度查一下,代码可以理解作配置内容,用户可以修改的是这个部分,
server.cpp
// 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
char buffer[1024];
// 注意是 while ,一直接收完毕
while (1)
{
int iret;
memset(buffer,0,sizeof(buffer));
if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) // 接收客户端的请求报文。
{
// 发送完毕后会执行这一句
printf("iret=%d\n",iret); break;
}
printf("接收:%s\n",buffer);
strcpy(buffer,"ok"); // 改变发送的字符或者数据
if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) // 向客户端发送响应结果。
{
perror("send"); break; }
printf("发送:%s\n",buffer);
}
// 第6步:关闭socket,释放资源。
close(listenfd); close(clientfd);
----------------------------------------------------------------------
分界线
----------------------------------------------------------------------
client.cpp
char buffer[1024];
// 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
// 发送完毕
for (int ii=0;ii<3;ii++)
{
int iret;
memset(buffer,0,sizeof(buffer));
// 对 buffer 写入数据,并发送,可以修改
sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1);
if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0) // 向服务端发送请求报文。
{
perror("send"); break; }
printf("发送:%s\n",buffer);
memset(buffer,0,sizeof(buffer));
// 接收数据,存到 buffer 里面
if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0) // 接收服务端的回应报文。
{
// 接收完毕,结束
printf("iret=%d\n",iret); break;
}
printf("接收:%s\n",buffer);
}
// 第4步:关闭socket,释放资源。
close(sockfd);
听课记录
使用 gdb 可以调试程序:
yum install gdb // 安装gdb
gdb server // 调试 server
(gdb) set args 5005 // 设置参数
(gdb) run // 运行程序
(gdb) n // 跳一行
(gdb) p sockfd // 查看某个变量值
(gdb) q // 退出
其他都在尝试中练习
通过对上面的学习,对网络编程有了一定的了解,上面基本都是固定格式,要记住他们的通信规律和方法,按照模板修改就可以,后面继续学习。
再次声明,我仅仅是用作学习记录,再分享我的学习过程,如果侵权我立马撤回。函数声明如下
int socket(int domain, int type, int protocol);
参数说明:
1. domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、
AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在
通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组
合、AF_UNIX决定了要用一个绝对路径名作为地址。
2. type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、
SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,
针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于
无连接的UDP服务应用。
3. protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、
IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
4. 返回值:成功则返回一个socket,失败返回-1,错误原因存于errno 中。
说了一大堆废话,第一个参数只能填AF_INET,第二个参数只能填SOCK_STREAM,第三个参数只能填
0。除非系统资料耗尽,socket函数一般不会返回失败。
缺省打开 socket 为 1024 ,这个是由系统设定决定的,搞线程压力测试需要注意。使用这个语句查看
ulimit -a
字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序,一个 32 位整数由 4 个字节组成。内存中存储这 4 个字节有两个方法:一种是将低序字节存储在起始地址,称为小端字节序;另外是将高字节存储在起始地址,称为大端字节序。比如
将 0x12345678
大端字节序
高地址 低地址
78 56 34 12
小端字节序
高地址 低地址
12 34 56 78
注意:
网络字节序:
网络字节序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而
保证数据在不同主机之间传输时能被正确解释。网络字节采用大端排序方式。
主机字节序:
不同的机器主机字节序不相同,与 CPU 设计有关,数据的顺序是由 CPU 决定的,与操作系统无关。
由于这个原因,不同体系结构的机器之间无法通信,所以要转换成一种约定的字节序,也就是网络字节
序。即使同一台机器上的两个进程(比如一个由于 C 语言,另外一个由 JAVA 编写)通信,也要考虑字
节序的问题(JAVA采用大段字节序)
网络字节序与主机字节序之间的转换函数:
// 完成 16 位无符号数的相互转换
htons() // host to network short
ntohs() // network to host short
// 完成 32 位无符号数的相互转换
htonl() // host to network long
ntohl() // network to host long
TCP 协议中的主机地址和端口采用整数来表示:
192.168.190.134
// 小端方式
11000000 10101000 10111110 10000110
3232284294 ----> 十进制数存放
// 大端方式 --- 网络字节序方式
10000110 10111110 10101000 11000000
2260641984 ----> 十进制数存放
网络编程中,网络协议,IP地址,端口是采用一个结构体存放的,其结构体如下,
// 两个结构体字节一样,因此可以互相强制转换类型
// 这样存放存在的问题:用 14 字节存放操作比较麻烦
struct sockaddr{
unsigned short sa_family; // 地址类型,AF_xxx 2 字节
char sa_data[14]; // 14字节的端口和地址
};
struct sockaddr_in{
short int sin_family; //地址类型 - 2 字节
unsigned short int sin_port;//端口号 - 2 字节
struct in_addr sin_addr; // 地址 - 4 字节
unsigned char sin_zero[8]; // 为了保持与 struct sockaddr 一样的长度 - 8 字节
};
struct in_addr{
unsigned long s_addr; // 地址
};
对 IP 地址进行处理存储的结构体
struct hostent{
char * h_name; // 主机名
char ** h_aliases; // 主机所有别名构成的字符串数组,同一 IP 可绑定多个域名
int h_addrtype; // 主机的 IP 地址的类型,例如 IPv4(AD_INET)还是 IPv6
int h_length; // 主机的 IP 地址长度,IPv4 地址为 4 , IPv6地址为 16
char ** h_addr_list; // 主机的 IP 地址,以网络字节序存储
#define h_addr h_addr_list[0];
};
// gethostbyname 函数可以利用字符串格式的域名获得 IP 网络字节顺序地址
struct hostent *gethostbyname(const char * name);
在这里,像这样写,无论给域名,还是 IP 地址都可以解析
struct hostent* h;
if ( (h = gethostbyname(argv[1])) == 0 ) // 指定服务端的ip地址。
{
printf("gethostbyname failed.\n"); close(sockfd); return -1; }
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
还有一些和结构体转换相对应的函数
1. int inet_aton(const *cp, struct in_addr *inp);
将一个字符串 IP 地址转换为一个 32 位的网络字节序 IP 地址。如果这个函数成功,函数的返回值
非零,如果输入地址不正确则会返回零。使用这个函数并没有错误码存在 errno 中,所以它的值会忽
略。
2. char *inet_ntoa(struct in_addr in);
把网络字节序转化为字符串的 IP 地址。
3. in_addr_t inet_addr(const char *cp);
把字符串 IP 地址转化为网络字节序。
bind函数把一个本地协议地址赋予一个套接字。对于网际协议,协议地址是32位的IPv4地址或是128位的IPv6地址与16位的TCP或UDP端口号的组合。服务端把用于通信的地址和端口绑定到socket上。函数声明如下:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
1. 参数sockfd,需要绑定的socket。
2. 参数addr,存放了服务端用于通信的地址和端口。注意结构体要强制转化为 sockaddr 类型
3. 参数addrlen表示addr结构体的大小。
4. 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
如果绑定的地址错误,或端口已被占用,bind函数一定会报错,否则一般不会返回错误。
对几个函数的总结
1). 服务器在调用 listen() 之前,客户端不能向服务端发起连接请求的。
2). 服务端调用 listen() 函数后,服务端的 socket 开始监听客户端的连接。
3). 客户端调用 connect() 函数向服务端发起连接请求。
4). 在 TCP 底层,客户端和服务端握手后建立起通信通道,如果有多个客户请求,在服务端就会形成
一个已准备好的连接的队列。
5). 服务端调用 accept() 函数从队列中获取一个已准备好的连接,函数返回一个新的 socket ,
新的 socket 用于与客户端通信,listen 的socket 只负责监听客户端的连接请求。
listen函数把主动连接socket变为被动连接的socket,使得这个socket可以接受其它socket的连接请求,从而成为一个服务端的socket。如果 socket 不进行 listen,那么就会连接错误。
int listen(int sockfd, int backlog);
返回:0-成功, -1-失败
1. 参数sockfd是已经被bind过的socket。socket函数返回的socket是一个主动连接的socket,
在服务端的编程中,程序员希望这个socket可以接受外来的连接请求,也就是被动等待客户端来连接。
由于系统默认时认为一个socket是主动连接的,所以需要通过某种方式来告诉系统,程序员通过调用
listen函数来完成这件事。
2. 参数backlog,这个参数涉及到一些网络的细节,比较麻烦,填5、10都行,一般不超过30。
3. 当调用listen之后,服务端的socket就可以调用accept来接受客户端的连接请求。
4. 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
connect函数向服务器发起连接请求,声明如下
int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);
1. 函数说明:connect函数用于将参数sockfd 的socket 连至参数serv_addr 指定的服务端,参
数addrlen为sockaddr的结构长度。
2. 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
3. connect函数只用于客户端。
如果服务端的地址错了,或端口错了,或服务端没有启动,connect一定会失败。
accept函数为服务端接受客户端的连接。对于多个 client ,建立通信时,accept 相当于从队列中依此接收 client 发送的消息,如果队列为空,则阻塞等待。其声明如下
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
1. 参数sockfd是已经被listen过的socket。
2. 参数addr用于存放客户端的地址信息,用sockaddr结构体表达,如果不需要客户端的地址,可以
填0。
3. 参数addrlen用于存放addr参数的长度,如果addr为0,addrlen也填0。
4. accept函数等待客户端的连接,如果没有客户端连上来,它就一直等待,这种方式称之为阻塞。
5. accept等待到客户端的连接后,创建一个新的socket,函数返回值就是这个新的socket,服务端 使用这个新的socket和客户端进行报文的收发。
6. 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
7. accept在等待的过程中,如果被中断或其它的原因,函数返回-1,表示失败,如果失败,可以重新accept。
三次握手图解如下,
其中 ESTABLISHED 代表握手成功,listen 完成以后,等待连接,变成 SYN_RECV 以后,可以接受发送数据。
可以通过这样查看对应端口状态
netstat -na|grep 5005
把 server.cpp 部分改为
// 第3步:把socket设置为监听模式。
if (listen(listenfd,5) != 0 ) {
perror("listen"); close(listenfd); return -1; }
sleep(1000);
// 第4步:接受客户端的连接。
while(1){
clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen);
printf("客户端(%s)已连接。\n",inet_ntoa(clientaddr.sin_addr));
sleep(10);
}
然后先启动服务端 ./server 5005
通过观察其一直在 listen 状态
然后启动多个客户端,都用 5005端口
./client 127.0.0.1 5005
结果如下
可以看出,服务端与客户端都握手成功。 在 listen 和 connect 建立握手以后,等待 accept 数据发送和接收。
现在介绍两个函数,发送数据和接受数据函数,其函数声明如下。
recv 函数用于接收对端 socket 发送过来的数据。recv 函数用于接收对端通过 socket 发送过来的数据。不论是客户端还是服务端,应用程序都用 recv 函数接收来自TCP 连接的另一端发送过来数据。声明如下
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
1. sockfd为已建立好连接的socket。
2. buf为用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,只要是一块内存就行了。
3. len需要接收数据的长度,不能超过buf的大小,否则内存溢出。
4. flags填0, 其他数值意义不大。
5. 函数返回已接收的字符数。出错时返回-1,失败时不会设置errno的值。
如果socket的对端没有发送数据,recv函数就会等待,如果对端发送了数据,函数返回接收到的字符
数。出错时返回-1。如果socket被对端关闭,返回值为0。如果recv函数返回的错误(<=0),表示通
信通道已不可用。
send函数用于把数据通过socket发送给对端。不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据。函数声明如下
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
1. sockfd为已建立好连接的socket。
2. buf为需要发送的数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、
字符串,内存中有什么就发送什么。
3. len需要发送的数据的长度,为buf中有效数据的长度。
4. flags填0, 其他数值意义不大。
5. 函数返回已发送的字符数。出错时返回-1,错误信息errno被标记。
注意,就算是网络断开,或socket已被对端关闭,send函数不会立即报错,要过几秒才会报错。如果
send函数返回的错误(<=0),表示通信链路已不可用。
send 函数也是有阻塞的,如果 buffer 被填满,接收端 recv 还没接收满足,那么就会阻塞。直到接收 recv 足够的时候,继续发送。
基本概念
分包:发送方发送字符串“helloworld”,接收方却收到两个字符串“hello”和“world”。
粘包:发送方发送两个字符串“hello”+“world”,接收方却一次性收到了“helloworld”。
但是 TCP 传输可以保证几点:
1. 顺序不变;
2. 分割的包中间不会插入其他数据。为解决分包和粘包问题,定义一份协议,常用方式为
报文长度 + 报文内容 0010helloworld
报文长度 ascii 码,二进制整数
关于 TCP 报文分包和粘包的情况,视频中有演示,我自己按照它的方法,在我的虚拟机上面运行,并没有出现杂乱的情况。不过 UP 主说后面会继续讲解,后面再继续理解。
socket 编程的函数很多,细节也很多,如果每个项目都从 socket 的函数开始编程,代码会非常繁琐。解决方法就是封装(造轮子)。
recv() 函数可能存在读取的报文不完整的情况,send() 也可能存在写入数据不完整的情况。因此写了两个函数了解这些问题,两个函数如下,
为了解决 TCP 分包和粘包的问题,用 TcpWrite 和 TcpRead 两个函数来解决问题
其源码如下,
/*
* 程序名:book248.cpp,此程序用于演示用C++的方法封装socket服务端
* 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include
#include
#include
#include
#include
#include
#include
class CTcpServer
{
public:
int m_listenfd; // 服务端用于监听的socket
int m_clientfd; // 客户端连上来的socket
CTcpServer();
bool InitServer(int port); // 初始化服务端
bool Accept(); // 等待客户端的连接
// 向对端发送报文
int Send(const void *buf,const int buflen);
// 接收对端的报文
int Recv(void *buf,const int buflen);
~CTcpServer();
};
int main()
{
CTcpServer TcpServer;
if (TcpServer.InitServer(5005)==false)
{
printf("TcpServer.InitServer(5005) failed,exit...\n"); return -1; }
if (TcpServer.Accept() == false) {
printf("TcpServer.Accept() failed,exit...\n"); return -1; }
printf("客户端已连接。\n");
char strbuffer[1024];
while (1)
{
memset(strbuffer,0,sizeof(strbuffer));
if (TcpServer.Recv(strbuffer,sizeof(strbuffer))<=0) break;
printf("接收:%s\n",strbuffer);
strcpy(strbuffer,"ok");
if (TcpServer.Send(strbuffer,strlen(strbuffer))<=0) break;
printf("发送:%s\n",strbuffer);
}
printf("客户端已断开连接。\n");
}
CTcpServer::CTcpServer()
{
// 构造函数初始化socket
m_listenfd=m_clientfd=0;
}
CTcpServer::~CTcpServer()
{
if (m_listenfd!=0) close(m_listenfd); // 析构函数关闭socket
if (m_clientfd!=0) close(m_clientfd); // 析构函数关闭socket
}
// 初始化服务端的socket,port为通信端口
bool CTcpServer::InitServer(int port)
{
m_listenfd = socket(AF_INET,SOCK_STREAM,0); // 创建服务端的socket
// 把服务端用于通信的地址和端口绑定到socket上
struct sockaddr_in servaddr; // 服务端地址信息的数据结构
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 本主机的任意ip地址
servaddr.sin_port = htons(port); // 绑定通信端口
if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
{
close(m_listenfd); m_listenfd=0; return false; }
// 把socket设置为监听模式
if (listen(m_listenfd,5) != 0 ) {
close(m_listenfd); m_listenfd=0; return false; }
return true;
}
bool CTcpServer::Accept()
{
if ( (m_clientfd=accept(m_listenfd,0,0)) <= 0) return false;
return true;
}
int CTcpServer::Send(const void *buf,const int buflen)
{
return send(m_clientfd,buf,buflen,0);
}
int CTcpServer::Recv(void *buf,const int buflen)
{
return recv(m_clientfd,buf,buflen,0);
}
其源码如下,
/*
* 程序名:book247.cpp,此程序用于演示用C++的方法封装socket客户端
* 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include
#include
#include
#include
#include
#include
#include
// TCP客户端类
class CTcpClient
{
public:
int m_sockfd;
CTcpClient();
// 向服务器发起连接,serverip-服务端ip,port通信端口
bool ConnectToServer(const char *serverip,const int port);
// 向对端发送报文
int Send(const void *buf,const int buflen);
// 接收对端的报文
int Recv(void *buf,const int buflen);
~CTcpClient();
};
int main()
{
CTcpClient TcpClient;
// 向服务器发起连接请求
if (TcpClient.ConnectToServer("127.0.0.1,5005) == false)
{
printf("TcpClient.ConnectToServer(\"127.0.0.1\",5005) failed,exit...\n"); return -1; }
char strbuffer[1024];
for (int ii=0;ii<5;ii++)
{
memset(strbuffer,0,sizeof(strbuffer));
sprintf(strbuffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1);
if (TcpClient.Send(strbuffer,strlen(strbuffer))<=0) break;
printf("发送:%s\n",strbuffer);
memset(strbuffer,0,sizeof(strbuffer));
if (TcpClient.Recv(strbuffer,sizeof(strbuffer))<=0) break;
printf("接收:%s\n",strbuffer);
}
}
CTcpClient::CTcpClient()
{
m_sockfd=0; // 构造函数初始化m_sockfd
}
CTcpClient::~CTcpClient()
{
if (m_sockfd!=0) close(m_sockfd); // 析构函数关闭m_sockfd
}
// 向服务器发起连接,serverip-服务端ip,port通信端口
bool CTcpClient::ConnectToServer(const char *serverip,const int port)
{
m_sockfd = socket(AF_INET,SOCK_STREAM,0); // 创建客户端的socket
struct hostent* h; // ip地址信息的数据结构
if ( (h=gethostbyname(serverip)) == 0 )
{
close(m_sockfd); m_sockfd=0; return false; }
// 把服务器的地址和端口转换为数据结构
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
// 向服务器发起连接请求
if (connect(m_sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0)
{
close(m_sockfd); m_sockfd=0; return false; }
return true;
}
int CTcpClient::Send(const void *buf,const int buflen)
{
return send(m_sockfd,buf,buflen,0);
}
int CTcpClient::Recv(void *buf,const int buflen)
{
return recv(m_sockfd,buf,buflen,0);
}
采用C++封装的意义主要有以下几方面:
1) 把数据初始化的代码放在构造函数中;
2) 把关闭socket等释放资源的代码放在析构函数中;
3) 把socket定义为类的成员变量,类外部的代码根本看不到socket;
4) 代码更简洁,更安全(析构函数自动调用关闭socket,释放资源)。
关于进程的几个重要函数,
// 1. getpid库函数的功能是获取本程序运行时进程的编号。
pid_t getpid();
// 2. fork函数用于产生一个新的进程,函数返回值pid_t是一个整数,在父进程中,返回值是子进程编号,在子进程中,返回值是0。
pid_t fork();
linux 对进程的相关操作:
ps 查看当前终端的进程。
ps -ef |grep book 查看系统全部的进程
子进程和父进程使用相同的代码段;子进程拷贝了父进程的堆栈段和数据段。子进程一旦开始运行,它复制了父进程的一切数据,然后各自运行,相互之间没有影响。 在父进程中定义的变量子进程中会复制一个副本,fork之后,子进程对变量的操作不会影响父进程,父进程对变量的操作也不会影响子进程。还需要注意,
1)进程的编号是系统动态分配的,相同的程序在不同的时间执行,进程的编号是不同的。
2)进程的编号会循环使用,但是,在同一时间,进程的编号是唯一的,也就是说,不管任何时间,系统
不可能存在两个编号相同的进程。
进程的数据空间是独立的,私有的,不能相互访问,但是在某些情况下进程之间需要通信来实现某功能或交换数据,包括:
1) 数据传输: 一个进程需要将它的数据发送给另一个进程。
2) 共享数据: 多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
3) 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如通知进程退出)。
4) 进程控制: 一个进程希望控制另一个进程的运行。
进程通信的方式分为以下几种,
1)管道:包括无名管道(pipe)及命名管道(named pipe),无名管道可用于具有父进程和子进程
之间的通信。命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲
缘关系进程间的通信。
2)消息队列(message):进程可以向队列中添加消息,其它的进程则可以读取队列中的消息。
3)信号(signal):信号用于通知其它进程有某种事件发生。
4)共享内存(shared memory):多个进程可以访问同一块内存空间。
5)信号量(semaphore):也叫信号灯,用于进程之间对共享资源进行加锁。
6)套接字(socket):可用于不同计算机之间的进程间通信。
应用经验:
1)管道太过时了,实在没什么应用价值,了解概念就行。
2)socket可以用于不同系统之间的进程通信,完全可以代替只能在同一系统中进程之间通信的管道和
消息队列。
3)信号的应用场景非常多,主要用于进程的控制,例如通知正在运行中的后台服务程序退出。
4)同一系统中,进程之间采用共享内存交换数据的效率是最高的,但是,共享内存没有加锁的机制,所
以经常与信号灯结合一起来使用,在高性能的网络服务端程序中,可以用共享内存作为的数据缓存
(cache)。
5)在企业IT系统内部,消息队列已经逐渐成为通信的核心手段,它具有低耦合、可靠投递、广播、流量
控制、一致性等一系列功能。当今市面上有很多主流的消息中间件有Redis、RabbitMQ、Kafka、
ActiveMQ、ZeroMQ,阿里巴巴自主开发RocketMQ等。
如果想让程序在后台运行,执行程序的时候,命令的最后面加“&”符号。程的数
比如之前运行客户端使用:
./client 127.0.0.1 5005 &
这样可以运行多个客户端
查看进程
ps -ef|grep client
killall client
把进程杀死
或者
先用“ps -ef|grep client”找到程序的进程编号,然后用“kill 进程编号”。
还可以采用fork,主程序执行fork,生成一个子进程,然后父进程退出,留下子进程继续运行,子进程将由系统托管。这样也在后台增加一个程序
if (fork()>0) return 0;
signal信号是Linux编程中非常重要的部分,接下来将详细介绍信号的基本概念、实现和使用,和与信号的几个系统调用(库函数)。signal信号是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。
软中断信号(signal,又简称为信号)用来通知进程发生了事件。 进程之间可以通过调用kill库函数发送软中断信号。Linux内核也可能给进程发送信号,通知进程发生了某个事件(例如内存越界)。
注意,信号只是用来通知某进程发生了什么事件,无法给进程传递任何数据,进程对信号的处理方法有三种:
1)第一种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。
2)第二种是设置中断的处理函数,收到信号后,由该函数来处理。
3)第三种方法是,对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程。
发出信号的原因很多,这里按发出信号的原因简单分类,以了解各种信号,参考链接:http://www.freecplus.net/eec5c39aa63b45ad946f1cc08134d9f9.html
signal库函数可以设置程序对信号的处理方式,如下,
sighandler_t signal(int signum, sighandler_t handler);
参数signum表示信号的编号。
参数handler表示信号的处理方式,有三种情况:
1)SIG_IGN:忽略参数signum所指的信号。
2)一个自定义的处理信号的函数,信号的编号为这个自定义函数的参数。
3)SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
程序员不关心signal的返回值
Linux操作系统提供了kill命令向程序发送信号,C语言也提供了kill库函数,用于在程序中向其它进
程或者线程发送信号。
int kill(pid_t pid, int sig);
参数pid 有几种情况:
1)pid>0 将信号传给进程号为pid 的进程。
2)pid=0 将信号传给和目前进程相同进程组的所有进程,常用于父进程给子进程发送信号,注意,发
送信号者进程也会收到自己发出的信号。
3)pid=-1 将信号广播传送给系统内所有的进程,例如系统关机时,会向所有的登录窗口广播关机信
息。
sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在运行。
返回值说明: 成功执行时,返回0;失败返回-1,errno被设为以下的某个值。
EINVAL:指定的信号码无效(参数 sig 不合法)。
EPERM:权限不够无法传送信号给指定进程。
ESRCH:参数 pid 所指定的进程或进程组不存在。
服务程序运行在后台,如果想让中止它,强行杀掉不是个好办法,因为程序被杀的时候,程序突然死亡,没有释放资源,会影响系统的稳定,用 Ctrl + C 中止与杀程序是相同的效果。信号的作用: 如果能向后台程序发送一个信号,后台程序收到这个信号后,调用一个函数,在函数中编写释放资源的代码,程序就可以有计划的退出, 安全而体面。
共享内存(Shared Memory) 就是允许多个进程访问同一个内存空间,是在多个进程之间共享和传递数据最高效的方式。操作系统将不同进程之间共享内存安排为同一段物理内存,进程可以将共享内存连接到它们自己的地址空间中,如果某个进程修改了共享内存中的数据,其它的进程读到的数据也将会改变,共享内存并未提供锁机制。如果要对共享内存的读/写加锁,可以使用信号灯。Linux中提供了一组函数用于操作共享内存,程序中需要包含以下头文件:
#include
#include
一些重要的函数,
shmget函数用来获取或创建共享内存,它的声明为:
int shmget(key_t key, size_t size, int shmflg);
参数key是共享内存的键值,是一个整数,typedef unsigned int key_t,是共享内存在系统中的编号,不同共享内存的编号不能相同,这一点由程序员保证。key用十六进制表示比较好。
参数size是待创建的共享内存的大小,以字节为单位。
参数shmflg是共享内存的访问权限,与文件的权限一样,0666|IPC_CREAT表示全部用户对它可读写,如果共享内存不存在,就创建一个共享内存。
把共享内存连接到当前进程的地址空间。它的声明如下:
void *shmat(int shm_id, const void *shm_addr, int shmflg);
参数shm_id是由shmget函数返回的共享内存标识。
参数shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
参数shm_flg是一组标志位,通常为0。
调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
该函数用于将共享内存从当前进程中分离,相当于shmat函数的反操作。它的声明如下:
int shmdt(const void *shmaddr);
参数shmaddr是shmat函数返回的地址。
调用成功时返回0,失败时返回-1.
删除共享内存,它的声明如下:
int shmctl(int shm_id, int command, struct shmid_ds *buf);
参数shm_id是shmget函数返回的共享内存标识符。
参数command填IPC_RMID。
参数buf填0。
解释一下,shmctl是控制共享内存的函数,其功能不只是删除共享内容,但其它的功能没什么用,所以不介绍了。
注意,用root创建的共享内存,不管创建的权限是什么,普通用户无法删除。
一个例子程序如下,
/*
* 程序名:book258.cpp,此程序用于演示共享内存的用法
* 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include
#include
#include
#include
#include
int main()
{
int shmid; // 共享内存标识符
// 创建共享内存,键值为0x5005,共1024字节。
if ( (shmid = shmget((key_t)0x5005, 1024, 0640|IPC_CREAT)) == -1)
{
printf("shmat(0x5005) failed\n"); return -1; }
char *ptext=0; // 用于指向共享内存的指针
// 将共享内存连接到当前进程的地址空间,由ptext指针指向它
ptext = (char *)shmat(shmid, 0, 0);
// 操作本程序的ptext指针,就是操作共享内存
printf("写入前:%s\n",ptext);
sprintf(ptext,"本程序的进程号是:%d",getpid());
printf("写入后:%s\n",ptext);
// 把共享内存从当前进程中分离
shmdt(ptext);
// 删除共享内存
// if (shmctl(shmid, IPC_RMID, 0) == -1)
// { printf("shmctl(0x5005) failed\n"); return -1; }
}
运行结果如下,
因为在程序中没有把共享内存删除,所以每次运行时候,都会把进程号写入共享内存,开始共享内存是空的,后来不断填充并覆盖。可以用
ipcs -m // 查看共享内存
ipcrm -m 编号(shmid) // 手动删除共享内存
信号量(信号灯)本质上是一个计数器,用于协调多个进程(包括但不限于父子进程)对共享数据对象的读/写。 它不以传送数据为目的,主要是用来保护共享资源(共享内存、消息队列、socket连接池、数据库连接池等),保证共享资源在一个时刻只有一个进程独享。信号量是一个特殊的变量,只允许进程对它进行等待信号和发送信号操作。 最简单的信号量是取值0和1的二元信号量,这是信号量最常见的形式。 通用信号量(可以取多个正整数值)和信号量集方面的知识比较复杂,应用场景也比较少。
Linux中提供了一组函数用于操作信号量,程序中需要包含以下头文件:
#include
#include
#include
相关函数如下,
semget函数用来获取或创建信号量,它的原型如下:
int semget(key_t key, int nsems, int semflg);
1)参数key是信号量的键值,typedef unsigned int key_t,是信号量在系统中的编号,不同信号量的编号不能相同,这一点由程序员保证。key用十六进制表示比较好。
2)参数nsems是创建信号量集中信号量的个数,该参数只在创建信号量集时有效,这里固定填1。
3)参数sem_flags是一组标志,如果希望信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。如果没有设置IPC_CREAT标志并且信号量不存在,就会返错误(errno的值为2,No such file or directory)。
4)如果semget函数成功,返回信号量集的标识;失败返回-1,错误原因存于error中。
该函数用来控制信号量(常用于设置信号量的初始值和销毁信号量),它的原型如下:
int semctl(int semid, int sem_num, int command, ...);
1)参数semid是由semget函数返回的信号量标识。
2)参数sem_num是信号量集数组上的下标,表示某一个信号量,填0。
3)参数cmd是对信号量操作的命令种类,常用的有以下两个:
IPC_RMID:销毁信号量,不需要第四个参数;
SETVAL:初始化信号量的值(信号量成功创建后,需要设置初始值),这个值由第四个参数决定。第四参数是一个自定义的共同体,如下:
// 用于信号灯操作的共同体。
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
4)如果semctl函数调用失败返回-1;如果成功,返回值比较复杂,暂时不关心它。
该函数有两个功能:1)等待信号量的值变为1,如果等待成功,立即把信号量的值置为0,这个过程也称之为等待锁;2)把信号量的值置为1,这个过程也称之为释放锁。
int semop(int semid, struct sembuf *sops, unsigned nsops);
1)参数semid是由semget函数返回的信号量标识。
2)参数nsops是操作信号量的个数,即sops结构变量的个数,设置它的为1(只对一个信号量的操作)。
3)参数sops是一个结构体,如下:
struct sembuf
{
short sem_num; // 信号量集的个数,单个信号量设置为0。
short sem_op; // 信号量在本次操作中需要改变的数据:-1-等待操作;1-发送操作。
short sem_flg; // 把此标志设置为SEM_UNDO,操作系统将跟踪这个信号量。
// 如果当前进程退出时没有释放信号量,操作系统将释放信号量,避免资源被死锁。
};
示例:
1)等待信号量的值变为1,如果等待成功,立即把信号量的值置为0;
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);
2)把信号量的值置为1。
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);
例子如下,
/*
* 程序名:book259.cpp,此程序用于演示信号量的使用方法。
* 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include
#include
#include
#include
#include
#include
class CSEM
{
private:
union semun // 用于信号灯操作的共同体。
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
int sem_id; // 信号灯描述符。
public:
bool init(key_t key); // 如果信号灯已存在,获取信号灯;如果信号灯不存在,则创建信号灯并初始化。
bool wait(); // 等待信号灯挂出。
bool post(); // 挂出信号灯。
bool destroy(); // 销毁信号灯。
};
int main(int argc, char *argv[])
{
CSEM sem;
// 初始信号灯。
if (sem.init(0x5000)==false) {
printf("sem.init failed.\n"); return -1; }
printf("sem.init ok\n");
// 等待信信号挂出,等待成功后,将持有锁。
if (sem.wait()==false) {
printf("sem.wait failed.\n"); return -1; }
printf("sem.wait ok\n");
sleep(50); // 在sleep的过程中,运行其它的book259程序将等待锁。
// 挂出信号灯,释放锁。
if (sem.post()==false) {
printf("sem.post failed.\n"); return -1; }
printf("sem.post ok\n");
// 销毁信号灯。
// if (sem.destroy()==false) { printf("sem.destroy failed.\n"); return -1; }
// printf("sem.destroy ok\n");
}
bool CSEM::init(key_t key)
{
// 获取信号灯。
if ( (sem_id=semget(key,1,0640)) == -1)
{
// 如果信号灯不存在,创建它。
if (errno==2)
{
if ( (sem_id=semget(key,1,0640|IPC_CREAT)) == -1) {
perror("init 1 semget()"); return false; }
// 信号灯创建成功后,还需要把它初始化成可用的状态。
union semun sem_union;
sem_union.val = 1;
if (semctl(sem_id,0,SETVAL,sem_union) < 0) {
perror("init semctl()"); return false; }
}
else
{
perror("init 2 semget()"); return false; }
}
return true;
}
bool CSEM::destroy()
{
if (semctl(sem_id,0,IPC_RMID) == -1) {
perror("destroy semctl()"); return false; }
return true;
}
bool CSEM::wait()
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1) {
perror("wait semop()"); return false; }
return true;
}
bool CSEM::post()
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1) {
perror("post semop()"); return false; }
return true;
}
结果如下,
可以看出,在两个程序运行时候,一个程序占用信号资源,在 init 以后,处于 wait 状态;另外一个运行以后,在 init 以后,处于挂起状态,在第一个程序 post 以后,第二个程序才处于 wait 状态。这里 init 是初始化以后,wait 是等待,但是占用了现在的信号资源,post 是挂出,释放了信号资源。
可以用
ipcs -s // 查看系统的信号量
ipcrm sem 8 // 手工删除信号量
和多进程相比,多线程是一种比较节省资源的多任务操作方式。 启动一个新的进程必须分配给它独立的地址空间,每个进程都有自己的堆栈段和数据段,系统开销比较高,进行数据的传递只能通过进行间通信的方式进行。 在同一个进程中,可以运行多个线程,运行于同一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享全局变量和对象,启动一个线程所消耗的资源比启动一个进程所消耗的资源要少。
在Linux下,采用pthread_create函数来创建一个新的线程,函数声明:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数thread为为指向线程标识符的地址。
参数attr用于设置线程属性,一般为空,表示使用默认属性。
参数start_routine是线程运行函数的地址,填函数名就可以了。
参数arg是线程运行函数的参数。新创建的线程从start_routine函数的地址开始运行,该函数只有一个无类型指针参数arg。若要想向start_routine传递多个参数,可以将多个参数放在一个结构体中,然后把结构体的地址作为arg参数传入,但是要非常慎重,程序员一般不会这么做。
在编译时注意加上-lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库。
如果进程中的任一线程调用了exit,则整个进程会终止,所以,在线程的start_routine函数中,不能采用exit。线程的终止有三种方式:1)线程的start_routine函数代码结束,自然消亡。2)线程的start_routine函数调用pthread_exit结束。3)被主进程或其它线程中止。pthread_exit函数的声明如下:
void pthread_exit(void *retval);
参数retval填空,即0
有一个例子,多线程的socket服务端,注意需要添加 socket 客户端程序实现通信,程序如下,
/*
* 程序名:book261.cpp,此程序用于演示多线程的socket通信服务端
* 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class CTcpServer
{
public:
int m_listenfd; // 服务端用于监听的socket
int m_clientfd; // 客户端连上来的socket
CTcpServer();
bool InitServer(int port); // 初始化服务端
bool Accept(); // 等待客户端的连接
// 向对端发送报文
int Send(const void *buf,const int buflen);
// 接收对端的报文
int Recv(void *buf,const int buflen);
// void CloseClient(); // 关闭客户端的socket,多线程服务端不需要这个函数。
// void CloseListen(); // 关闭用于监听的socket,多线程服务端不需要这个函数。
~CTcpServer();
};
CTcpServer TcpServer;
// SIGINT和SIGTERM的处理函数
void EXIT(int sig)
{
printf("程序退出,信号值=%d\n",sig);
close(TcpServer.m_listenfd); // 手动关闭m_listenfd,释放资源
exit(0);
}
// 与客户端通信线程的主函数
void *pth_main(void *arg);
int main()
{
// 忽略全部的信号
for (int ii=0;ii<50;ii++) signal(ii,SIG_IGN);
// 设置SIGINT和SIGTERM的处理函数
signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
if (TcpServer.InitServer(5005)==false)
{
printf("服务端初始化失败,程序退出。\n"); return -1; }
while (1)
{
if (TcpServer.Accept() == false) continue;
pthread_t pthid; // 创建一线程,与新连接上来的客户端通信
if (pthread_create(&pthid,NULL,pth_main,(void*)((long)TcpServer.m_clientfd))!=0)
{
printf("创建线程失败,程序退出。n"); return -1; }
printf("与客户端通信的线程已创建。\n");
}
}
CTcpServer::CTcpServer()
{
// 构造函数初始化socket
m_listenfd=m_clientfd=0;
}
CTcpServer::~CTcpServer()
{
if (m_listenfd!=0) close(m_listenfd); // 析构函数关闭socket
if (m_clientfd!=0) close(m_clientfd); // 析构函数关闭socket
}
// 初始化服务端的socket,port为通信端口
bool CTcpServer::InitServer(int port)
{
if (m_listenfd!=0) {
close(m_listenfd); m_listenfd=0; }
m_listenfd = socket(AF_INET,SOCK_STREAM,0); // 创建服务端的socket
// 把服务端用于通信的地址和端口绑定到socket上
struct sockaddr_in servaddr; // 服务端地址信息的数据结构
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 本主机的任意ip地址
servaddr.sin_port = htons(port); // 绑定通信端口
if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
{
close(m_listenfd); m_listenfd=0; return false; }
// 把socket设置为监听模式
if (listen(m_listenfd,5) != 0 ) {
close(m_listenfd); m_listenfd=0; return false; }
return true;
}
bool CTcpServer::Accept()
{
if ( (m_clientfd=accept(m_listenfd,0,0)) <= 0) return false;
return true;
}
int CTcpServer::Send(const void *buf,const int buflen)
{
return send(m_clientfd,buf,buflen,0);
}
int CTcpServer::Recv(void *buf,const int buflen)
{
return recv(m_clientfd,buf,buflen,0);
}
// 与客户端通信线程的主函数
void *pth_main(void *arg)
{
int clientfd=(long) arg; // arg参数为新客户端的socket。
// 与客户端通信,接收客户端发过来的报文后,回复ok。
char strbuffer[1024];
while (1)
{
memset(strbuffer,0,sizeof(strbuffer));
if (recv(clientfd,strbuffer,sizeof(strbuffer),0)<=0) break;
printf("接收:%s\n",strbuffer);
strcpy(strbuffer,"ok");
if (send(clientfd,strbuffer,strlen(strbuffer),0)<=0) break;
printf("发送:%s\n",strbuffer);
}
printf("客户端已断开连接。\n");
close(clientfd); // 关闭客户端的连接。
pthread_exit(0);
}
文件命名为 Thread.cpp ,并把 client.cpp 拷在文件下,其中 makefile 文件如下,
all:client Thread
client:client.cpp
g++ -g -o client client.cpp
Thread:Thread.cpp
g++ -g -o Thread Thread.cpp -lpthread
1. 对信号的处理,设置处理为 Ctrl+c,kill 信号量时,使用 EXIT 函数:
// 设置SIGINT和SIGTERM的处理函数
signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
2. 注意在 pth_main 中作为线程执行的函数,语法在上面提到过,但是注意参数的类型转
换。
线程有joinable和unjoinable两种状态,如果线程是joinable状态,当线程主函数终止时(自己退出或调用pthread_exit退出)不会释放线程所占用内存资源和其它资源,这种线程被称为“僵尸线程”。创建线程时默认是非分离的,或者称为可连接的(joinable)。避免僵尸线程就是如何正确的回收线程资源,有四种方法:
方法一:创建线程后,在创建线程的程序中调用pthread_join等待线程退出,一般不会采用这种方法,因为pthread_join会发生阻塞。
pthread_join(pthid,NULL);
2)方法二:创建线程前,调用pthread_attr_setdetachstate将线程设为detached,这样线程退出时,系统自动回收线程资源。
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); // 设置线程的属性。
pthread_create(&pthid,&attr,pth_main,(void*)((long)TcpServer.m_clientfd);
3)方法三:创建线程后,在创建线程的程序中调用pthread_detach将新创建的线程设置为detached状态。
pthread_detach(pthid);
4)方法四:在线程主函数中调用pthread_detach改变自己的状态。
pthread_detach(pthread_self());
锁大概有两种:一种是不允许访问;另一种是资源忙,同一时间只允许一个使用者占用,其它使用者必须要等待。 对多线程来说,资源是共享的,基本上不存在不允许访问的情况,但是,共享的资源在某一时间点只能有一个线程占用,所以需要给资源加锁。
线程的锁的种类有互斥锁、读写锁、条件变量、自旋锁、信号灯。学习中,只介绍互斥锁,其它的锁应用场景复杂,开发难度很大,不合适初学者。
互斥锁机制是同一时刻只允许一个线程占有共享的资源。
1. 初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
其中参数 mutexattr 用于指定锁的属性(见下),如果为NULL则使用缺省属性。
互斥锁的属性在创建锁的时候指定,当资源被某线程锁住的时候,其它的线程在试图加锁时表现将不同。当前有四个值可供选择:
1)PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
2)PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。
3)PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。
4)PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,等待解锁后重新竞争。
2、阻塞加锁
int pthread_mutex_lock(pthread_mutex *mutex);
如果是锁是空闲状态,本线程将获得这个锁;如果锁已经被占据,本线程将排队等待,直到成功的获取锁。
3、非阻塞加锁
int pthread_mutex_trylock( pthread_mutex_t *mutex);
该函数语义与 pthread_mutex_lock() 类似,不同的是在锁已经被占据时立即返回 EBUSY,不是挂起等待。
4、解锁
int pthread_mutex_unlock(pthread_mutex *mutex);
线程把自己持有的锁释放
5. 销毁锁(此时锁必需unlock状态,否则返回EBUSY)
int pthread_mutex_destroy(pthread_mutex *mutex);
销毁锁之前,锁必需是空闲状态(unlock)。
多线程可以共享资源(变量和对象),对编程带来了方便,但是某些对象虽然可以共享,但在同一个时间只能由一个线程使用,多个线程同时使用会产生冲突,例如 socket 连接,数据库连接池。源程序如下,因为作者没有把 CTcpserver 类加入,我后来加入了运行出来,
/*
* 程序名:book263.cpp,此程序用于演示多线程的互斥锁
* 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include
#include
#include
#include
#include
#include
#include
#include
class CTcpServer
{
public:
int m_listenfd; // 服务端用于监听的socket
int m_clientfd; // 客户端连上来的socket
CTcpServer();
bool InitServer(int port); // 初始化服务端
bool Accept(); // 等待客户端的连接
// 向对端发送报文
int Send(const void *buf,const int buflen);
// 接收对端的报文
int Recv(void *buf,const int buflen);
// void CloseClient(); // 关闭客户端的socket,多线程服务端不需要这个函数。
// void CloseListen(); // 关闭用于监听的socket,多线程服务端不需要这个函数。
~CTcpServer();
};
CTcpServer TcpServer;
//xx pthread_mutex_t mutex; // 申明一个互斥锁
// 与客户端通信线程的主函数
void *pth_main(void *arg)
{
int pno=(long)arg; // 线程编号
pthread_detach(pthread_self());
char strbuffer[1024];
for (int ii=0;ii<3;ii++) // 与服务端进行3次交互。
{
//xx pthread_mutex_lock(&mutex); // 加锁
memset(strbuffer,0,sizeof(strbuffer));
sprintf(strbuffer,"线程%d:这是第%d个超级女生,编号%03d。",pno,ii+1,ii+1);
if (TcpClient.Send(strbuffer,strlen(strbuffer))<=0) break;
printf("发送:%s\n",strbuffer);
memset(strbuffer,0,sizeof(strbuffer));
if (TcpClient.Recv(strbuffer,sizeof(strbuffer))<=0) break;
printf("线程%d接收:%s\n",pno,strbuffer);
//xx pthread_mutex_unlock(&mutex); // 释放锁
// usleep(100); // usleep(100),否则其它的线程无法获得锁。
}
pthread_exit(0);
}
int main()
{
// 向服务器发起连接请求
if (TcpClient.ConnectToServer("172.16.0.15",5051)==false)
{
printf("TcpClient.ConnectToServer(\"172.16.0.15\",5051) failed,exit...\n"); return -1; }
//xx pthread_mutex_init(&mutex,0); // 创建锁
pthread_t pthid1,pthid2;
pthread_create(&pthid1,NULL,pth_main,(void*)1); // 创建第一个线程
pthread_create(&pthid2,NULL,pth_main,(void*)2); // 创建第二个线程
pthread_join(pthid1,NULL); // 等待线程1退出。
pthread_join(pthid2,NULL); // 等待线程2退出。
//xx pthread_mutex_lock(&mutex); // 销毁锁
}
CTcpServer::CTcpServer()
{
// 构造函数初始化socket
m_listenfd=m_clientfd=0;
}
CTcpServer::~CTcpServer()
{
if (m_listenfd!=0) close(m_listenfd); // 析构函数关闭socket
if (m_clientfd!=0) close(m_clientfd); // 析构函数关闭socket
}
// 初始化服务端的socket,port为通信端口
bool CTcpServer::InitServer(int port)
{
if (m_listenfd!=0) {
close(m_listenfd); m_listenfd=0; }
m_listenfd = socket(AF_INET,SOCK_STREAM,0); // 创建服务端的socket
// 把服务端用于通信的地址和端口绑定到socket上
struct sockaddr_in servaddr; // 服务端地址信息的数据结构
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 本主机的任意ip地址
servaddr.sin_port = htons(port); // 绑定通信端口
if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
{
close(m_listenfd); m_listenfd=0; return false; }
// 把socket设置为监听模式
if (listen(m_listenfd,5) != 0 ) {
close(m_listenfd); m_listenfd=0; return false; }
return true;
}
bool CTcpServer::Accept()
{
if ( (m_clientfd=accept(m_listenfd,0,0)) <= 0) return false;
return true;
}
int CTcpServer::Send(const void *buf,const int buflen)
{
return send(m_clientfd,buf,buflen,0);
}
int CTcpServer::Recv(void *buf,const int buflen)
{
return recv(m_clientfd,buf,buflen,0);
}
运行结果如下,
看出,如果客户端接收的数据,是存在乱序的,没有规律,因此给她们加上锁,注意,上面注释的部分取消就行,是这几个语句,
..... 全局变量
pthread_mutex_t mutex; // 申明一个互斥锁
.....线程函数
pthread_mutex_lock(&mutex); // 加锁
.....线程函数
pthread_mutex_unlock(&mutex); // 释放锁
usleep(100); // usleep(100),否则其它的线程无法获得锁。
.....主函数
pthread_mutex_init(&mutex,0); // 创建锁
.....
pthread_mutex_lock(&mutex); // 销毁锁
后面对网络编程知识继续深入学习,并进行复现,修改总结。