线程传参详解,detach()大坑,成员函数做线程函数

第三节 线程传参详解,detach()大坑,成员函数做线程函数

在这里插入图片描述

一、传递临时对象作为线程参数
1.1要避免的陷阱1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <thread>
using namespace std;
void myPrint(const int &i, char* pmybuf)
{
//如果线程从主线程detach了
//i不是mvar真正的引用,实际上值传递,即使主线程运行完毕了,子线程用i仍然是安全的,但仍不推荐传递引用
//推荐改为const int i
cout << i << endl;
//pmybuf还是指向原来的字符串,所以这么写是不安全的
cout << pmybuf << endl;
}
int main()
{
int mvar = 1;
int& mvary = mvar;
char mybuf[] = "this is a test";
thread myThread(myPrint, mvar, mybuf);//第一个参数是函数名,后两个参数是函数的参数
myThread.join();
//myThread.detach();
cout << "Hello World!" << endl;
}

1.2要避免的陷阱2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <thread>
#include <string>
using namespace std;
void myPrint(const int i, const string& pmybuf)
{
cout << i << endl;
cout << pmybuf << endl;
}
int main()
{
int mvar = 1;
int& mvary = mvar;
char mybuf[] = "this is a test";
//如果detach了,这样仍然是不安全的
//因为存在主线程运行完了,mybuf被回收了,系统采用mybuf隐式类型转换成string
//推荐先创建一个临时对象thread myThread(myPrint, mvar, string(mybuf));就绝对安全了。。。。
thread myThread(myPrint, mvar, mybuf);
myThread.join();
//myThread.detach();
cout << "Hello World!" << endl;
}

1.3总结

  • 如果传递int这种简单类型,推荐使用值传递,不要用引用
  • 如果传递类对象,避免使用隐式类型转换,全部都是创建线程这一行就创建出临时对象,然后在函数参数里,用引用来接,否则还会创建出一个对象
  • 终极结论:建议不使用detach

二、临时对象作为线程参数继续讲
2.1线程id概念

  • id是个数字,每个线程(不管是主线程还是子线程)实际上都对应着一个数字,而且每个线程对应的这个数字都不一样
  • 线程id可以用C++标准库里的函数来获取。std::this_thread::get_id()来获取

三、传递类对象、智能指针作为线程参数
3.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <thread>
using namespace std;
class A {
public:
mutable int m_i; //m_i即使实在const中也可以被修改
A(int i) :m_i(i) {}
};
void myPrint(const A& pmybuf)
{
pmybuf.m_i = 199;
cout << "子线程myPrint的参数地址是" << &pmybuf << "thread = " << std::this_thread::get_id() << endl;
}
int main()
{
A myObj(10);
//myPrint(const A& pmybuf)中引用不能去掉,如果去掉会多创建一个对象
//const也不能去掉,去掉会出错
//即使是传递的const引用,但在子线程中还是会调用拷贝构造函数构造一个新的对象,
//所以在子线程中修改m_i的值不会影响到主线程
//如果希望子线程中修改m_i的值影响到主线程,可以用thread myThread(myPrint, std::def(myObj));
//这样const就是真的引用了,myPrint定义中的const就可以去掉了,类A定义中的mutable也可以去掉了
thread myThread(myPrint, myObj);
myThread.join();
//myThread.detach();
cout << "Hello World!" << endl;
}

3.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <thread>
#include <memory>
using namespace std;
void myPrint(unique_ptr<int> ptn)
{
cout << "thread = " << std::this_thread::get_id() << endl;
}
int main()
{
unique_ptr<int> up(new int(10));
//独占式指针只能通过std::move()才可以传递给另一个指针
//传递后up就指向空,新的ptn指向原来的内存
//所以这时就不能用detach了,因为如果主线程先执行完,ptn指向的对象就被释放了
thread myThread(myPrint, std::move(up));
myThread.join();
//myThread.detach();
return 0;
}

四、用成员函数指针做线程函数
放在了2.2

大数据

  1. 大数据是什么?:
    全新的思维方式和商业模式
  2. 大数据的定义:
    广义:物理世界到数字世界的映射和提炼,发现数据的特征做出提升效率的决策行为
    狭义:获取、储存、分析,从大数据中挖掘价值的全新技术架构
  3. 明白三件事:
    3.1大数据要做什么
    获取、储存、分析数据
    3.2对谁来做
    对大容量数据操作
    3.3目的是什么
    挖掘价值
  4. 大数据有多大:
    大数据是PB和EB级别,1PB=1024TB,1EB=1024PB
  5. 大数据的四个V
    Volume-海量化
    Variety-多样化
    Velocity-时效性
    Value-价值密度
  6. 大数据的价值:
    数据就是财富,大数据开始走进我们的生活,其价值的高度前所未有。
    方面一:帮助企业了解用户
    方面二:帮助企业了解自己
  7. 大数据和云计算:
    大数据本身是资产,云计算是为了挖掘资产价值提供合适的工具
  8. 所需技能
    学好基础和框架,学好java、python
  9. 推荐书籍
    《数据之巅》-徐子沛
    《大数据时代》
    《大数据技术原理与应用》-林子雨

线程启动、结束,创建线程多法、join,detach

第二节 线程启动、结束,创建线程多法、join,detach

在这里插入图片描述

一、范例演示线程运行的开始

  • 程序运行起来,生成一个进程,该进程所属的主线程开始自动运行;当主线程从main()函数返回,则整个进程执行完毕
  • 主线程从main()开始执行,那么我们自己创建的线程,也需要从一个函数开始运行(初始函数),一旦这个函数运行完毕,线程也结束运行
  • 整个进程是否执行完毕的标志是:主线程是否执行完,如果主线程执行完毕了,就代表整个进程执行完毕了,此时如果其他子线程还没有执行完,也会被强行终止【此条有例外,以后会解释】

创建一个线程:

  1. 包含头文件thread
  2. 写初始函数
  3. 在main中创建thread

必须要明白:有两个线程在跑,相当于整个程序中有两条线在同时走,即使一条被阻塞,另一条也能运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <thread>
using namespace std;
void myPrint()
{
cout << "我的线程开始运行" << endl;
//-------------
//-------------
cout << "我的线程运行完毕" << endl;
return;
}
int main()
{
//(1)创建了线程,线程执行起点(入口)是myPrint;(2)执行线程
thread myThread(myPrint);
//(2)阻塞主线程并等待myPrint执行完,当myPrint执行完毕,join()就执行完毕,主线程继续往下执行
//join意为汇合,子线程和主线程回合
myThread.join();
//设置断点可看到主线程等待子线程的过程
//F11逐语句,就是每次执行一行语句,如果碰到函数调用,它就会进入到函数里面
//F10逐过程,碰到函数时,不进入函数,把函数调用当成一条语句执行
//(3)传统多线程程序中,主线程要等待子线程执行完毕,然后自己才能向下执行
//detach:分离,主线程不再与子线程汇合,不再等待子线程
//detach后,子线程和主线程失去关联,驻留在后台,由C++运行时库接管
//myThread.detach();
//(4)joinable()判断是否可以成功使用join()或者detach()
//如果返回true,证明可以调用join()或者detach()
//如果返回false,证明调用过join()或者detach(),join()和detach()都不能再调用了
if (myThread.joinable())
{
cout << "可以调用可以调用join()或者detach()" << endl;
}
else
{
cout << "不能调用可以调用join()或者detach()" << endl;
}
cout << "Hello World!" << endl;
return 0;
}

重要补充:

线程类参数是一个可调用对象。
一组可执行的语句称为可调用对象,c++中的可调用对象可以是函数、函数指针、lambda表达式、bind创建的对象或者重载了函数调用运算符的类对象。

二、其他创建线程的方法
①创建一个类,并编写圆括号重载函数,初始化一个该类的对象,把该对象作为线程入口地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Ta
{
public:
void operator()() //不能带参数
{
cout << "我的线程开始运行" << endl;
//-------------
//-------------
cout << "我的线程运行完毕" << endl;
}
};
//main函数里的:
Ta ta;
thread myThread(ta);
myThread.join();

②lambda表达式创建线程

1
2
3
4
5
6
7
8
9
10
//main函数中
auto lambdaThread = [] {
cout << "我的线程开始执行了" << endl;
//-------------
//-------------
cout << "我的线程开始执行了" << endl;
};
thread myThread(lambdaThread);
myThread.join();

③把某个类中的某个函数作为线程的入口地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Data_
{
public:
void GetMsg(){}
void SaveMsh(){}
};
//main函数里
Data_ s;
//第一个&意思是取址,第二个&意思是引用,相当于std::ref(s)
//thread oneobj(&Data_::SaveMsh,s)传值也是可以的
//在其他的构造函数中&obj是不会代表引用的,会被当成取地址
thread oneobj(&Data_::SaveMsh,&s);
thread twoobj(&Data_::GetMsg,&s);
oneobj.join();
twoobj.join();

互斥量概念、用法、死锁演示及解决详解

第五节 互斥量概念、用法、死锁演示及解决详解

在这里插入图片描述

一、互斥量(mutex)的基本概念

  • 互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。
  • 互斥量使用要小心,保护数据不多也不少,少了达不到效果,多了影响效率。

二、互斥量的用法
包含#include 头文件
2.1 lock(),unlock()

  • 步骤:1.lock(),2.操作共享数据,3.unlock()。
  • lock()和unlock()要成对使用

2.2 lock_guard类模板

  • lock_guard sbguard(myMutex);取代lock()和unlock()
  • lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()

三、死锁
3.1 死锁演示
死锁至少有两个互斥量mutex1,mutex2。

  • a.线程A执行时,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。
  • b.线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1.
  • c.此时,死锁产生了,A锁着mutex1,需要锁mutex2,B锁着mutex2,需要锁mutex1,两个线程没办法继续运行下去。。。

3.2 死锁的一般解决方案:
只要保证多个互斥量上锁的顺序一样就不会造成死锁。

3.3 std::lock()函数模板

  • std::lock(mutex1,mutex2……); 一次锁定多个互斥量(一般这种情况很少),用于处理多个互斥量。
  • 如果互斥量中一个没锁住,它就等着,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)

3.4 std::lock_guard的std::adopt_lock参数

  • std::lock_guardstd::mutex my_guard(my_mutex,std::adopt_lock);
    加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();
  • adopt_guard为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要在lock()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A{
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
//lock_guard<mutex> sbguard(myMutex1, adopt_lock);
lock(myMutex1, myMutex2);
//myMutex2.lock();
//myMutex1.lock();
msgRecvQueue.push_back(i);
myMutex1.unlock();
myMutex2.unlock();
}
}
}
bool outMsgLULProc()
{
myMutex1.lock();
myMutex2.lock();
if (!msgRecvQueue.empty())
{
cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素" << msgRecvQueue.front() << endl;
msgRecvQueue.pop_front();
myMutex2.unlock();
myMutex1.unlock();
return true;
}
myMutex2.unlock();
myMutex1.unlock();
return false;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
if (outMsgLULProc())
{
}
else
{
cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex myMutex1;
mutex myMutex2;
};
int main()
{
A myobja;
mutex myMutex;
thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
}

并发基本概念及实现,进程、线程基本概念

一 并发基本概念及实现,进程、线程基本概念

在这里插入图片描述

一、并发、进程、线程的基本概念和综述
并发,线程,进程要求必须掌握

1.1 并发

  • 两个或者更多的任务(独立的活动)同时发生(进行):一个程序同时执行多个独立的任务;
  • 以往计算机,单核cpu(中央处理器):某一个时刻只能执行一个任务,由操作系统调度,每秒钟进行多次所谓的“任务切换”。并发的假象(不是真正的并发),切换(上下文切换)时要保存变量的状态、执行进度等,存在时间开销;
  • 随着硬件发展,出现了多处理器计算机:用于服务器和高性能计算领域。台式机:在一块芯片上有多核(一个CPU内有多个运算核心,对于操作系统来说,每个核心都是作为单独的CPU对待的):双核,4核,8核,10核(自己的笔记本是4核8线程的)。能够实现真正的并行执行多个任务(硬件并发)
  • 使用并发的原因:主要就是同时可以干多个事,提高性能

1.2 可执行程序

  • 磁盘上的一个文件,windows下,扩展名为.exe;linux下,ls -la,rwx(可读可写可执行)

1.3 进程

  • 运行一个可执行程序,在windows下,可双击;在linux下,./文件名
  • 进行,一个可执行程序运行起来了,就叫创建了一个进程。进程就是运行起来的可执行程序。

1.4 线程

  • a)每个进程(执行起来的可执行程序),都有唯一的一个主线程
  • b)当执行可执行程序时,产生一个进程后,这个主线程就随着这个进程默默启动起来了
  • ctrl+F5运行这个程序的时候,实际上是进程的主线程来执行(调用)这个main函数中的代码
  • 线程:用来执行代码的。线程这个东西,可以理解为一条代码的执行通路
    在这里插入图片描述

  • 除了主线程之外,可以通过写代码来创建其他线程,其他线程走的是别的道路,甚至区不同的地方
  • 每创建一个新线程,就可以在同一时刻,多干一个不同的事(多走一条不同的代码执行路径)

  • 多线程(并发)
  • 线程并不是越多越好,每个线程,都需要一个独立的堆栈空间(大约1M),线程之间的切换要保存很多中间状态,切换也会耗费本该属于程序运行的时间

必须使用多线程的案例
在这里插入图片描述
在这里插入图片描述

1.5 学习心得

  • 开发多线程程序:一个是实力的体现,一个是商用的必须需求
  • 线程开发有一定难度
  • C++线程会设计很多新概念
  • 网络方向:网络通讯、网络服务器,多线程是绝对绕不开的

二、并发的实现方法

实现并发的手段:
a)通过多个进程实现并发
b)在单独的进程中,写代码创建除了主线程之外的其他线程来实现并发

2.1 多进程并发

  • 比如账号服务器一个进程,游戏服务器一个进程。
  • 服务器进程之间存在通信(同一个电脑上:管道,文件,消息队列,共享内存);(不同电脑上:socket通信技术)

2.2 多线程并发

  • 线程:感觉像是轻量级的进程。每个进程有自己独立的运行路径,但一个进程中的所有线程共享地址空间(共享内存),全局变量、全局内存、全局引用都可以在线程之间传递,所以多线程开销远远小于多进程
  • 多进程并发核多线程并发可以混合使用,但建议优先考虑多线程技术
  • 本课程中只讲多线程并发技术

三、C++11新标准线程库
以往

  • windows:CreateThread(), _beginthread(),_beginthreadexe()创建线程;linux:pthread_create()创建线程;不能跨平台
  • 临界区,互斥量
  • POSIX thread(pthread):跨平台,但要做一番配置,也不方便

C++11

  • 从C++11新标准,C++语言本身增加对多线程的支持,意味着可移植性(跨平台),这大大减少开发人员的工作量

socket编程学习笔记(2)

在这里插入图片描述
在这里插入图片描述

TCP客户/服务器模型
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

struct sockaddr是一个通用地址,如果用ipv4,需要将ipv4的地址结构struct sockaddr_in强制转换为通用的地址结构
在这里插入图片描述

套接字一旦传递给listen,就变成了被动套接字。主动套接字会调用connect()函数发起连接,被动套接字会调用accept()函数接受连接。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

write() 的原型为:

1
ssize_t write(int fd, const void *buf, size_t nbytes);

fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。
write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。

read() 的原型为:

1
ssize_t read(int fd, void *buf, size_t nbytes);

fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。
read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

close函数是用来关闭套接字

1
int close(int sockfd);

成功返回0,出错为-1

在这里插入图片描述

实现一对一的客户/服务器回射:

echosrv.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//
// Created by hh on 20-05-08.
//
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0);
int main(int argc, char** argv) {
// 1. 创建套接字
int listenfd;
//三个参数分别是通信协议族,socket类型,协议类型
if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
// 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET; //地址组
servaddr.sin_port = htons(5188); //端口号,端口号是2个字节,htons中的s(short)就表示32位
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //ip地址,INADDR_ANY表示本机的任一地址
// servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //显式指定ip地址
// inet_aton("127.0.0.1", &servaddr.sin_addr); //和上一个效果相同
/*
int on = 1;
// 确保time_wait状态下同一端口仍可使用
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on) < 0)
{
ERR_EXIT("setsockopt");
}
*/
// 2. 绑定套接字地址
//三个参数分别是socket返回的套接字,要绑定的地址,地址长度
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof servaddr) < 0) {
ERR_EXIT("bind");
}
// 3. 等待连接请求状态
//两个参数分别是socket返回的套接字,规定内核为此套接字排队的最大连接个数,可以填个数字
if (listen(listenfd, SOMAXCONN) < 0) { //SOMAXCONN表示队列的最大值
ERR_EXIT("listen");
}
//定义一个对等方的套接字地址
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof peeraddr;
// 4. 允许连接
int connfd;
//功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞
//可以这么理解,peeraddr有一个ip地址,很多端口,而连接的对象是ip地址和端口组成都套接字
//返回的connfd就是已连接的套接字
//三个参数分别是服务器套接字,返回对等方的套接字地址,返回对等方的套接字地址长度
if ((connfd = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) {
ERR_EXIT("accept");
}
printf("id = %s, ", inet_ntoa(peeraddr.sin_addr));
printf("port = %d\n", ntohs(peeraddr.sin_port));
// 5. 数据交换
char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof recvbuf);
//从connfd中读取sizeof(recvbuf)字节到把缓冲区recvbuf中,成功则返回写入的字节数,失败则返回 -1。
int ret = read(connfd, recvbuf, sizeof recvbuf);
if (ret == 0)
{
} else
{
}
fputs(recvbuf, stdout);
//把缓冲区recvbuf中的ret个字节写入connfd
write(connfd, recvbuf, ret);
}
// 6. 断开连接
close(connfd);
close(listenfd);
return 0;
}

echocli.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//
// Created by hh on 20-05-08.
//
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0);
int main(int argc, char** argv) {
// 1. 创建套接字
int sock;
//三个参数分别是通信协议族,socket类型,协议类型
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
// 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET; //地址组
servaddr.sin_port = htons(5188); //端口号,端口号是2个字节,htons中的s(short)就表示32位
//127.0.0.1是一个回送地址,指本地机,一般用来测试使用。
//常用来ping 127.0.0.1来看本地ip/tcp正不正常,如能ping通即可正常使用。
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //显式指定ip地址
if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("connect lalala");
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(sock, sendbuf, strlen(sendbuf));
read(sock, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf)); //清空缓存区
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sock);
return 0;
}

Makefile

1
2
3
4
5
6
7
8
9
.PHONY:clean all
CC=gcc
CFLAGS=-Wall -g
BIN=echosrv echocli
all:$(BIN)
%.o:%.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f *.o $(BIN)

首先启动服务器echosrv,再启动客户端echocli。在客户端发送一行消息后,服务器会收到一条消息,随后服务器又会把消息原封不动的发回客户端。

即在客户端发送一行消息,执行的是如图所示的过程
在这里插入图片描述

补充:send()/recv()和write()/read():发送数据和接收数据

在 Linux 和 Windows 平台下,使用不同的函数发送和接收 socket 数据,下面我们分别讲解。

Linux下数据的接收和发送
Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。

前面我们说过,两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。

  • write() 的原型为:
1
ssize_t write(int fd, const void *buf, size_t nbytes);

fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。
size_t 是通过 typedef 声明的 unsigned int 类型;ssize_t 在 “size_t” 前面加了一个”s”,代表 signed,即 ssize_t 是通过 typedef 声明的 signed int 类型。
write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。

  • read() 的原型为:
1
ssize_t read(int fd, void *buf, size_t nbytes);

fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。

read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

Windows下数据的接收和发送
Windows 和 Linux 不同,Windows 区分普通文件和套接字,并定义了专门的接收和发送的函数。

从服务器端发送数据使用 send() 函数,它的原型为:

1
int send(SOCKET sock, const char *buf, int len, int flags);

sock 为要发送数据的套接字,buf 为要发送的数据的缓冲区地址,len 为要发送的数据的字节数,flags 为发送数据时的选项。

返回值和前三个参数不再赘述,最后的 flags 参数一般设置为 0 或 NULL,初学者不必深究。

在客户端接收数据使用 recv() 函数,它的原型为:

1
int recv(SOCKET sock, char *buf, int len, int flags);

socket编程学习笔记(1)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

大端字节序就是在内存中先存储数字的高位,这样数字的高位就存储在了内存的低地址处,小端字节序就是在内存中先存储数字的低位。
socket可以实现异构系统间通信,不同的硬件平台对整数的存放形式是不一样的,有的采用大端字节序,有的采用小端字节序。这时候必须统一字节序,统一的字节序称为网络字节序,先将发送方主机上的字节序转换为网络字节序,接收方收到后再转换为自己的字节序。

补充:x86或80x86是intel首先开发制造的一种微处理器体系结构的泛称,而基于这种微处理器体系结构搭建起来的硬件平台就成为x86平台,windows和linux都是基于x86平台的。
补充:%0x和%x都是以十六进制格式输出
vim是vi的升级版本

在这里插入图片描述
在这里插入图片描述

把32位的主机字节序转换为网络字节序
把16位的主机字节序转换为网络字节序
把32位的网络字节序转换为主机字节序
把32位的网络字节序转换为主机字节序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
unsigned int x = 0x12345678;
unsigned char* p1 = (unsigned char*)&x;
printf("%0x %0x %0x %0x\n", p1[0], p1[1], p1[2], p1[3]);
unsigned int y = htonl(x);
unsigned char* p2 = (unsigned char*)&y;
printf("%0x %0x %0x %0x\n", p2[0], p2[1], p2[2], p2[3]);
return 0;
}

输出结果是
78 56 34 12
12 34 56 78

在这里插入图片描述
老版本的转换函数

1
2
3
4
5
6
7
8
9
inet_aton、inet_aton、inet_ntoa在点分十进制(如206.168.112.96)与它长度为32位的网络字节序二进制值间转换IPv4地址
#include "arpa/inet.h"
int inet_aton(const char *strptr,struct in_addr *addrptr);
/*若字符串有效返回1,否则返回0*/
in_addr_t inet_addr(const char *strptr);
/*若字符串有效则为32位二进制网络字节序的IPv4地址,否则为INADDR_NONE*/
char *inet_ntoa(struct in_addr inadrr);
/*返回指向一个点分十进制数串的指针*/

现在使用inet_pton和inet_ntop函数
inet_pton 和 inet_ntop 这两个函数是随IPv6出现的新函数,对于IPv4地址和IPv6地址都适用。目前更多的是用这两函数进行地址转换。

在这里插入图片描述

在这里插入图片描述

gcc和Makefile

makefile的作用就是帮助我们便捷的编译。

掌握makefile首先要略懂gcc编译的一些简单指令

gcc的编译选项可以记做 ESc(很像我们的离开键)

1.预处理,生成预编译文件(i.文件):
    gcc –E hello.c –o hello.i
2.编译,生成汇编代码(.s文件):
    gcc –S hello.i –o hello.s
3.汇编,生成目标文件(.o文件):
    gcc –c hello.s –o hello.o
4.链接,生成可执行文件:
    gcc hello.o –o hello

如果不想搞这些东西当然也可以一步搞定,什么参数都不加,
gcc -o hello hello.c 或 gcc hello.c -o hello

其实makefile就是让我们把这些东西写到一个文件里,并且你要有自己的逻辑顺序和关系,makefile根据这些逻辑关系决定执行哪些指令。

还是以上面那个例子说明

假设我们还是hello.c文件生成的目标文件是hello,如果使用makefile要这样做:
1.vim Makefile 或 gedit Makefile

2.Makefile中这么写
hello:hello.c

gcc hello.c -o hello

3.make

注意gcc前面是一个tab键,上面两行的意思是,(hello依赖hello.c文件,执行下一行命令)

命令不止可以写一行,如我们在后面再加一行,touch hello(touch命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件),虽然看起来是和编译毫不相干的命令,但是还是会执行

 hello:hello.c                              #标签:依赖
 <tab>gcc hello.c -o hello                     (tab) 终端要执行的命令(可以有多行)
<tab> touch hello

我们可以看到目录下面多了一个hello文件。

现在我们懂了,makefile第一行 标签:依赖 的意思就是,生成标签 ,查看标签的依赖,如果依赖不是最新,编译该依赖的命令,如果依赖已经是最新,则执行这一个标签下的命令。

如果我们make后面不接参数,则默认第一个标签。

当然,makefile还有许多高级用法,如变量,函数之类,但是不是这里要细讲的内容,总之明白makefile的本质,并且精通gcc的命令,就可以建立一些小的工程。

C++顶层const和底层const

顶层const表示对象本身是一个常量
底层const表示不能通过指针或引用改变所指向的对象

例如

1
2
3
4
5
6
7
const int a = 42; //顶层const,不能改变a的值,int只能有顶层const
//--------------------------指针------------------------------
int b = 12;
const int* b1 = &b; //底层const,不能通过b1改变a的值
int* const b2 = &b; //顶层const,b2的值本身不能改变,即不能指向其他对象
//--------------------------引用------------------------------
const int& c = a; //底层const,不能通过引用改变a的值,用于声明引用的const都是底层const

第一个要注意的是

1
2
3
4
5
6
//对于一个常量只能用指向const的指针指向它
int* b3 = &a; //错误
//对于一个常量也只能进行常量引用
int& c2 = 3; //错误
int& c2 = 3 * b; //错误

第二个要注意的是

1
2
3
4
5
6
7
8
9
//顶层const在声明是必须初始化
const int a;//错误
int* const b2; //错误
const int* b1;
b1 = &b; //可以
//当然引用在声明是也必须初始化
const int& c; // 错误

5G到底是什么

0G:军事上,在两个军事据点上利用AM和FM两个波段传输语音信号
1G (1980-1990) 代表产品是大哥大,是模拟信号,很容易被窃听
2G(1990-2000):开始传送数字信号,可以加密和实现全球漫游,支持短信

  • 2.5G :GPRS技术
  • 2.75G:EDGE技术,可以收发邮件,浏览简单网页,聊QQ,发彩信

3G (2000-2010):带动移动互联网兴起,苹果,安卓系统,微博,微信,支持在移动中上网

  • 3.5G和3.75G:HSPA和HSPA+技术,产品为WCDMA

4G(2010-2020):移动互联网成熟期,以传输数据为核心,数据延迟降低为二三十ms(上网看视频网速重要,即时性游戏延迟重要),大型对战手游空前发展,王者荣耀,吃鸡等

  • 4.5G:
  • 4.9G:LTE技术

5G(2020-2030?):

  1. 速度是4G的10倍以上,
  2. 延迟进一步降低,1毫秒左右,应用如:远程医疗,工业控制自动驾驶,
  3. 低功耗,支持超过1万亿的设备同时接入,物联网智能家居等(以前被认为不能上网的东西可能可以上网,如水表电表,接入网络就不用每月人为的查看数据)

5G的网络切片技术:对于每个连接到5G网络的设备,根据所需要的网络需求,定制所需要的网络。1)游戏用户关心的是延迟足够低,2)VR、8k高画质电影的用户,网速特别重要,2)一部分的物联网设备对网速和延迟都不重要,对成本要足够低,需要时要能够立即响应。

很多年前就有人提出,在5G时代有可能会得到推广的技术:运算中心化,建立一个中心的超级计算机,手机和电脑都连接在超级计算机上,由超级计算机完成中心化的运算,再把运算结果返回给用户。

高通设立的专利墙。收取的专利费会根据手机的零售价来定。中国在2G时代没有什么话语权,从3G开始有所进步。

5G什么时候可以使用:采用了更短的波段,绕射能力很差,传播过程中衰减更大,所以需要更密集的基站,但基站成本比4G更低,更小,隐藏性更好,只有一台空调挂机那么大。