冬枭

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

1.队列,链表,STL

1.C++ STL标准库简介

长久以来,软件界一直希望建立一种可重复利用的东西,以及一种得以制造出”可重复运用的东西”

的方法,从函数(functions),类别(classes),函数库(function libraries),类别库(class libraries)、各种

组件,从模块化设计,到面向对象(object oriented ),为的就是复用性的提升。

复用性必须建立在某种标准之上。但是在许多环境下,就连软件开发最基本的数据结构(data

structures) 和算法(algorithm)都未能有一套标准。大量程序员被迫从事大量重复的工作,竟然是为

了完成前人已经完成而自己手上并未拥有的程序代码,这不仅是人力资源的浪费,也是挫折与痛苦

的来源。

为了建立数据结构和算法的一套标准,并且降低他们之间的耦合关系,以提升各自的独立性、弹

性、交互操作性(相互合作性,interoperability),诞生了STL。

STL(Standard Template Library,标准模板库),是惠普实验室开发的一系列软件的统称。现在主要

出现在 c++中,但是在引入 c++之前该技术已经存在很长时间了。

STL 从广义上分为: 容器(container) 算法(algorithm) 迭代器(iterator)。

容器和算法之间通过迭代器进行无缝连接。STL 几乎所有的代码都采用了模板类或者模板函数,这

相比传统的由函数和类组成的库来说提供了更好的代码重用机会。

STL(Standard Template Library)标准模板库,在我们 c++标准程序库中隶属于 STL 的占到了 80%以

上。

 

 

vector 容器和数组array的区别很大,vector是动态空间,而array是静态。

vector随着元素的假如,它的内部机制会自动扩充空间以容纳新元素。因此vector的运用对于内存的合理利用与运用的灵活性有很大帮助,我们再也不必害怕空间不足而一开始就要求一个大块头array

 

#include "stdafx.h"
#include <iostream>
 
#include <vector>//动态数组
using namespace std;
 
int main()
{
   vector<int>  v1;//构造一个空的vector
   cout << "容量为:"<<v1.capacity() << "     元素个数: " << v1.size() << endl;
 
   vector<int>  v2(5);//构造一个空间大小为5,并且元素为5个(有默认值)的vector
   cout << "容量为:" << v2.capacity() << "     元素个数: " << v2.size() <<"       v2[0]:"<<v2[0]<< endl;
 
   vector<int>  v3(5,111);//构造一个空间大小为5,并且元素为5个(每个元素初始值为111)的vector
   cout << "容量为:" << v3.capacity() << "     元素个数: " << v3.size() << "       v3[0]:" << v3[0] << endl;
 
   vector<int>  v4(v3);//拷贝构造vector
   cout << "容量为:" << v4.capacity() << "     元素个数: " << v4.size() << "       v4[0]:" << v4[0] << endl;
 
   //像数组一样的访问vector
   v2[0] = 1;//vector重载了[]运算符
   v2[1] = 2;
   v2[2] = 3;
   v2.at(3) = 4;
   v2.at(4) = 5;
 
   for (size_t i = 0; i < v2.size(); i++)
   {
       //cout << v2[i] << "    ";
       //cout << v2.at(i)<< "    ";
        cout << &v2[i] << "    ";//输出地址,说明存储空间是连续的
   }
   cout << endl;
 
   return 0;
}

 

迭代器

下图是使用迭代器遍历元素实例

 

 

vector操作接口

 

 动态增删改查

#include<iostream>
#include<vector>
using namespace  std;
 
int main()
{
   vector<int >   v;//定义一个空的动态数组
   cout << "容量:" << v.capacity() << "        元素个数:" << v.size() << endl;
 
   //往尾部插入元素   //    1
   v.push_back(1);
   cout << "容量:" << v.capacity() << "        元素个数:" << v.size() << endl;
    
   v.push_back(2);     //      1  2 
   cout << "容量:" << v.capacity() << "        元素个数:" << v.size() << endl;
 
   //向某一个迭代器指向的位置插入
   v.insert(v.begin(), 3);//    3  1   2
   cout << "容量:" << v.capacity() << "        元素个数:" << v.size() << endl;
 
   //向某一个迭代器指向的位置插入2个值为4的元素
   v.insert(v.end()-1 ,  2, 4);//    3  1      4  4  2
   cout << "容量:" << v.capacity() << "        元素个数:" << v.size() << endl;
 
   //使用迭代器遍历
   for (vector<int >::const_iterator   it =  v.cbegin();   it!=  v.cend(); it++)
   {
       cout << *it << "        ";
   }
   cout << endl;
 
   //访问第一个元素
   cout <<"front        " <<v.front() << endl;
   //访问最后一个元素
   cout << "back        " << v.back() << endl;
   //访问某一个下标的元素
   cout << "at            " << v.at(3) << endl;
 
   //删除最后一个元素
   v.pop_back();
   cout << "容量:" << v.capacity() << "        元素个数:" << v.size() << endl;
 
   //删除开头的元素
   v.erase(v.begin());
   cout << "容量:" << v.capacity() << "        元素个数:" << v.size() << endl;
 
   //删除结尾的元素, end()指向最后一个元素的下一个
   v.erase(v.end()-1);
   cout << "容量:" << v.capacity() << "        元素个数:" << v.size() << endl;
 
   //使用迭代器遍历
   for (vector<int >::const_iterator it = v.cbegin(); it != v.cend(); it++)
   {
       cout << *it << "        ";
   }
   cout << endl;
 
   //删除所有元素,不会清除容量
   v.clear();
   cout << "容量:" << v.capacity() << "        元素个数:" << v.size() << endl;
 
   /*
   当size和capacity相等时继续添加数据,否则vector会扩容,
   每次扩容都是增加当前空间的1/2(第一次除外);
   */
   {
       vector<int>  v;
 
       cout << "------------------------capacity容量随元素个数size增加的规律----------------------------" << endl;
       for (int i = 0; i < 50; i++)
       {
           v.push_back(i);
           cout << "v的容量:" << v.capacity() << "  元素个数:" << v.size() << endl;
       }
   }
 
   return 0;
}

 

List链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针

链接次序实现的。

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包

括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

相较于vector的连续线性空间,list就显得负责许多,它的好处是每次插入或者删除一个元素,就是

配置或者释放一个元素的空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,

对于任何位置的元素插入或元素的移除,list永远是常数时间。

List和vector是两个最常被使用的容器。

List容器是一个双向链表。

 

 

#include<iostream>
 
#include<list>
using namespace std;
 
int main()
{
   list<int> l;//空的双向链表
   cout << "元素个数:" << l.size() << endl;
 
   list<int> l2(5);//初始化5个元素,默认值为类型的默认值
   cout << "元素个数:" << l2.size() <<"  "<<  * (l2.begin() )<< endl;
 
   list<int> l3(5,111);//初始化5个元素,每个元素初始值为111
   cout << "元素个数:" << l3.size() << "  " << *(l3.begin()) << endl;
 
   list<int> l4( l3 );//拷贝构造
   cout << "元素个数:" << l4.size() << "  " << *(l4.begin()) << endl;
 
   //不支持[]运算符,因为效率低
   //cout << l4[0] << endl;
 
   //验证了list容器的内存空间是不连续的
   for (list<int>::iterator   it = l3.begin(); it !=l3.end(); it++)
   {
       cout << &(*it) << "   ";
   }
   cout << endl;
 
   return 0;
}

增删改插

#include<iostream>
 
#include<list>
using namespace std;
 
int main()
{
   list<int>  l;
 
   //头部插入一个节点(list容器肯定知道头部的位置)
   l.push_front(111);
 
   //尾部插入一个节点(list容器肯定知道尾部的位置)
   l.push_back(444);
   l.push_back(555);
 
   //在某个迭代器的位置之前插入
   l.insert(l.begin(), 222);
 
   //在某个迭代器的位置之前插入n个相同值元素
   l.insert(l.begin(), 3,333);
 
   //访问链表第一个元素
   l.front() = 1;
   cout <<"第一个元素:"<< l.front() << endl; 
 
   //访问链表最后一个元素
   cout << "最后一个元素:" << l.back() << endl;
 
   for (list<int>::iterator it = l.begin();  it!= l.end(); it++)
   {
       cout << *it << "       ";
   }
   cout << endl;
 
   //删除链表头的元素
   l.pop_front();
 
   //删除链表尾的元素
   l.pop_back();
 
   //删除某个迭代器指向的元素
   l.erase(l.begin());
    
    //删除一段迭代器区间
   l.erase(l.begin(),l.end());
 
   //清空链表
   l.clear();
 
   for (list<int>::iterator it = l.begin(); it != l.end(); it++)
   {
       cout << *it << "       ";
   }
   cout << endl;
 
   return 0;
}

 

 

map容器

map(映射)是一种存储键值(key-value)对, key是唯一的、有序的 关联容器。

multimap(多重映射)是一种存储键值(key-vauekey是 允许重复的、有序的 关联容器。

map内部结构采用红黑树(Red-Black tree)平衡二叉树,它可以在O(log n)时间内高效的做查

找,插入和删除。

Map和list拥有相同的某些性质,当对它的容器元素进行新增操作或者删除操作时,操作之前的所

有迭代器,在操作完成之后依然有效,当然被删除的那个元素的迭代器必然是个例外。

Multimap和map的操作类似,唯一区别multimap键值可重复。

 

 

#include<map>
 
#include<iostream>
using namespace  std;
 
int main()
{
   //map映射 ,  每个元素都是 key-value 键值对,  key不能重复,value可以,有序的
   map<int, string>  m;//构造空的map
   cout <<"元素个数:"<< m.size() << endl;
 
   //初始化列表构造map
   map<int, string>  m2 = { {3,"CCC"} ,  {1,"AAA"} ,  {2,"BBB"} };
   cout << "元素个数:" << m2.size() << endl;
 
   //拷贝构造
   map<int, string>  m3(m2);
   cout << "元素个数:" << m3.size() << endl;
 
   //验证map容器中的元素类型   pair<int, string>
   cout << typeid(map<int, string>::value_type).name() << endl;
 
   //一对值  pair 类模板
   pair<int, float>  p1;
   p1.first = 1;
   p1.second = 2.34f;
 
   pair<int, float>  p2(2, 3.45f); //有参构造
   cout << "first: " << p2.first << "     second:" << p2.second<< endl;
 
   //使用make_pair函数构造pair
   pair<short, char>  p3 = make_pair<short, char>(3, 'A');
   //pair<short, char>  p3 = make_pair(3, 'A');//自动推导
   cout << "first: " << p3.first << "     second:" << p3.second << endl;
 
 
   map<int, string>  m4 = {   pair<int, string>( 3,"CCC" )   ,  make_pair( 2,"BBB"),  make_pair(1,"AAA"), };
   cout << "元素个数:" << m4.size() << endl;
 
   return 0;
}

插入,删除元素

#include<vector>
#include<map>
 
#include<iostream>
using namespace  std;
 
int main()
{
   //map映射 关联容器 ,  每个元素都是 key-value 键值对  pair, 
   //key不能重复,value可以, 有序的
   map<int, string>  m;//构造空的map 
 
   //插入pair
   pair<int, string>  p1(2, "BBB");
   m.insert(p1);
    
   m.insert(pair<int, string>(1, "AAA"));
 
   //可以通过insert返回值的 成员.second查看是否插入成功,true成功,false是失败
   m.insert( make_pair<int, string>(2, "bbb")); //插入重复的key, 插入失败
   m.insert(make_pair(3, "CCC")); 
 
   // 插入其它容器中迭代器范围中的元素
   vector<pair<int, string>>  v = { {3,"ccc"}, {5, "EEE"},{ 4,"DDD" },{6, "FFF"} };
   m.insert(v.begin(), v.end());
 
   //[key]= value 
   m[7] = "GGG"; //对于不存在的key, 插入,相当于 (7,"GGG") 
   m[8];  //对于不存在的key,插入,相当于 ( 8,"")
   m[2] = "bbb";// 已经存在的key,相当于是修改元素的value
 
   for (map<int, string>::iterator  it = m.begin(); it!= m.end(); ++it)
   {
       cout << it->first << "->" << it->second.c_str() << "   ";
   }
   cout << endl;
 
   //查找key为3的元素,成功返回迭代器,失败返回end()
   {
       map<int, string>::iterator  it = m.find(33);
       if (it != m.end())
       {
           cout << "找到:" << it->first << "->" << it->second.c_str() << endl;
       }
       else
       {
           cout << "未找到!" << endl;
       }
   }
 
   //[key] 如果key存在,直接返回value
   //陷阱 ,如果key不存在,他会自动插入key,value为默认值再返回
   cout << "[key] "<< m[33].c_str() << endl;
    
   for (map<int, string>::iterator it = m.begin(); it != m.end(); ++it)
   {
       cout << it->first << "->" << it->second.c_str() << "   ";
   }
   cout << endl;
 
   //删除key为3的元素
   m.erase(3); 
 
   //删除某个迭代器指向的元素
   m.erase(m.begin());
 
   //删除迭代器区间[)的元素
   map<int, string>::iterator it = m.begin();
   ++it; ++it; ++it; //往后移动
   m.erase(m.begin(), it); 
 
   //删除所有元素
   m.clear();
 
   for (map<int, string>::iterator it = m.begin(); it != m.end(); ++it)
   {
       cout << it->first << "->" << it->second.c_str() << "   ";
   }
   cout << endl;
 
   return 0;
}

 

 

2.线程

1.每个进程都有一个主线程,主线程随着进程默默启动

2.线程都要有函数入口,main函数即为主线程的入口

3.线程不是越多越好,每个线程需要一个独立的堆栈空间(大约1M),线程切换要保存很多中间状态,耗费时间

4.线程是轻量级的进程,一个进程中的线程共享地址空间,全局变量,全局内存,全局引用都可以在线程之间传递,所以多线操的开销远远小于多进程。

 

线程的创建和启动

void myPrint()
{
    // this_thread::get_id()可以获取当前执行线程的ID(独一无二)
    cout << "thread id " << this_thread::get_id() << " is starting..." << endl;
    //-------------
    cout << "thread is finished" << endl;
    return;
}

int main()
{
    //创建子线程1,指定线程执行入口是myPrint;(2)执行线程
    thread myThread1(myPrint);
    
    //阻塞主线程,当子线程1执行完毕再开始执行
    myThread1.join();

    //子线程1结束后创建子线程2,指定线程执行入口是myPrint;(2)执行线程
    thread myThread2(myPrint);

    //detach后,子线程和主线程失去关联,独立运行,
    //如果主线程先结束,则子线程驻留在后台,由C++运行时库接管
    //所以子线程2的打印不一定可以出现在终端上,取决于它和主线程执行的快慢
    myThread2.detach();
    
    cout << "Hello World!" << endl;
    return 0;
}

 

 

3.互斥锁,等待锁

多个资源访问同一资源时,为了保证数据的一致性,最简单的方式就是使用mutex(互斥锁)

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>


std::mutex g_mutex;
int g_count = 0;

void Counter() {
  g_mutex.lock();

  int i = ++g_count;
  std::cout << "count: " << i << std::endl;

  // 前面代码如有异常,unlock 就调不到了。
  g_mutex.unlock();
}

int main() {
  const std::size_t SIZE = 4;

  // 创建一组线程。
  std::vector<std::thread> v;
  v.reserve(SIZE);

  for (std::size_t i = 0; i < SIZE; ++i) {
    v.emplace_back(&Counter);
  }

  // 等待所有线程结束。
  for (std::thread& t : v) {
    t.join();
  }

  return 0;
}

Mutex2

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

std::mutex g_mutex;
int g_count = 0;

void Counter() {
  // lock_guard 在构造函数里加锁,在析构函数里解锁。
  std::lock_guard<std::mutex> lock(g_mutex);

  int i = ++g_count;
  std::cout << "count: " << i << std::endl;
}

int main() {
  const std::size_t SIZE = 4;

  std::vector<std::thread> v;
  v.reserve(SIZE);

  for (std::size_t i = 0; i < SIZE; ++i) {
    v.emplace_back(&Counter);
  }

  for (std::thread& t : v) {
    t.join();
  }

  return 0;
}

 

 

 

 

 

4.socket

 

解释了TCP/IP体系结构以及TCP协议的大概内容后就可以来说一说什么是Socket了。还是先来看一下百度百科对于Socket的介绍:套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

我们将一个小区比作一台计算机,一台计算机里面跑了很多程序,怎么区分程序呢,用的是端口,就好像小区用门牌号区分每一户人家一样。手机送到小明家了,怎么进去呢?从大门进啊,怎么找到大门呢?门牌号呀。不就相当于从互联网来的数据找到接收端计算机后再根据端口判断应该给哪一个程序一样吗。小明家的入口就可以用小区地址+门牌号进行唯一表示,那么同样的道理,程序也可以用IP+端口号进行唯一标识。那么这个程序的入口就被称作Socket。

现在再来说说什么是Socekt编程,我们将TCP协议简化一下,就只有三个核心功能:建立连接、发送数据以及接收数据。我们再来看一下Java中提供的Socket类中的核心功能:

 

 

 TCP的通信原理

 

 

 

1. 网络中进程之间如何通信?

2.Socket是什么?

3.socket的基本操作

socket()函数

bind()函数

listen(),connect()函数

accept()函数

read() ,write()函数

close()函数

socket中TCP的三次握手建立连接详情

socket中TCP的四次握手释放连接详解

6.一个例子

1.网络进程之间如何通信?

本地进程间通信IPC有很多方式,但可以总结为一下4类:

消息传递(管道,FIFO,消息队列)

同步(互斥量,条件变量,读写锁,文件和写记录锁,信号量)

共享内存(匿名和具名的)

远程过程调用

进程通信的前提是要唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程

,但是在网络中这是行不通的。其实TCP/IP协议族以加帮我们解决了这个问题,网络层的ip地址就可以唯一标识网络中的株机,而传输层的协议+端口可以唯一标识一台主机中的应用程序(进程),这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信可以利用这个标志与其它进程进行交互。

使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket),来实现网络进程之间的通信。就目前而言,几乎所有的应用都是采用socket,而现在又是网络时代,网络中进程通信是无处不在的,这就是我为什么说一切皆socket

 

2.什么是socket

上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。

2.1 socket()函数
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

 

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

 

bind()函数
正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数的三个参数分别为:

sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};

struct in_addr {
uint32_t s_addr;
};
ipv6对应的是:
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};

struct in6_addr {
unsigned char s6_addr[16];
};
Unix域对应的是:
#define UNIX_PATH_MAX 108

struct sockaddr_un {
sa_family_t sun_family;
char sun_path[UNIX_PATH_MAX];
};
addrlen:对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

 

网络字节序与主机字节序
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

  a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

  b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

所以: 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于 这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再 赋给socket。

 

 

3.3、listen()、connect()函数
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

3.4、accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

3.5、read()、write()等函数
万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:

read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:

   #include 

   ssize_t read(int fd, void *buf, size_t count);
   ssize_t write(int fd, const void *buf, size_t count);

   #include 
   #include 

   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);

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

   ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
   ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

 

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是 全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。

其它的我就不一一介绍这几对I/O函数了,具体参见man文档或者baidu、Google,下面的例子中将使用到send/recv。

 

close()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

#include
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

4、socket中TCP的三次握手建立连接详解
我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

客户端向服务器发送一个SYN J
服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
客户端再想服务器发一个确认ACK K+1
只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:

 

 

 

图1、socket中发送的TCP三次握手

从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。

5、socket中TCP的四次握手释放连接详解
上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:

 

 

 

图2、socket中发送的TCP四次握手

图示过程如下:

某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。

6.下面给出实现的一个实例
首先,先给出实现的截图

 

 

 

服务器端代码如下:
[cpp] view plaincopyprint?

 

#include "InitSock.h"
#include
#include
using namespace std;
CInitSock initSock; // 初始化Winsock库
int main()
{
// 创建套节字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//用来指定套接字使用的地址格式,通常使用AF_INET
//指定套接字的类型,若是SOCK_DGRAM,则用的是udp不可靠传输
//配合type参数使用,指定使用的协议类型(当指定套接字类型后,可以设置为0,因为默认为UDP或TCP)
if(sListen == INVALID_SOCKET)
{
printf("Failed socket() \n");
return 0;
}
// 填充sockaddr_in结构 ,是个结构体
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567); //1024 ~ 49151:普通用户注册的端口号
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 绑定这个套节字到一个本地地址
if(::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf("Failed bind() \n");
return 0;
}
// 进入监听模式
//2指的是,监听队列中允许保持的尚未处理的最大连接数
if(::listen(sListen, 2) == SOCKET_ERROR)
{
printf("Failed listen() \n");
return 0;
}
// 循环接受客户的连接请求
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
SOCKET sClient = 0;
char szText[] = " TCP Server Demo! \r\n";
while(sClient==0)
{
// 接受一个新连接
//((SOCKADDR*)&remoteAddr)一个指向sockaddr_in结构的指针,用于获取对方地址
sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
if(sClient == INVALID_SOCKET)
{
printf("Failed accept()");
}
printf("接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));
continue ;
}
while(TRUE)
{
// 向客户端发送数据
gets(szText) ;
::send(sClient, szText, strlen(szText), 0);
// 从客户端接收数据
char buff[256] ;
int nRecv = ::recv(sClient, buff, 256, 0);
if(nRecv > 0)
{
buff[nRecv] = '\0';
printf(" 接收到数据:%s\n", buff);
}
}
// 关闭同客户端的连接
::closesocket(sClient);
// 关闭监听套节字
::closesocket(sListen);
return 0;
}

 

 

客户端代码如下

#include "InitSock.h"
#include
#include
using namespace std;
CInitSock initSock; // 初始化Winsock库
int main()
{
// 创建套节字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(s == INVALID_SOCKET)
{
printf(" Failed socket() \n");
return 0;
}
// 也可以在这里调用bind函数绑定一个本地地址
// 否则系统将会自动安排
// 填写远程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 注意,这里要填写服务器程序(TCPServer程序)所在机器的IP地址
// 如果你的计算机没有联网,直接使用127.0.0.1即可
servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if(::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
printf(" Failed connect() \n");
return 0;
}
char buff[256];
char szText[256] ;
while(TRUE)
{
//从服务器端接收数据
int nRecv = ::recv(s, buff, 256, 0);
if(nRecv > 0)
{
buff[nRecv] = '\0';
printf("接收到数据:%s\n", buff);
}
// 向服务器端发送数据
gets(szText) ;
szText[255] = '\0';
::send(s, szText, strlen(szText), 0) ;
}
// 关闭套节字
::closesocket(s);
return 0;
}

封装的InitSock.h
[cpp] view plaincopyprint?
#include
#include
#include
#include
#pragma comment(lib, "WS2_32") // 链接到WS2_32.lib
class CInitSock
{
public:
CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer, majorVer);
if(::WSAStartup(sockVersion, &wsaData) != 0)
{
exit(0);
}
}
~CInitSock()
{
::WSACleanup();
}
};

 

 

5.管道通信

为什么需要进程通信IPC

互斥导致了异步,保证同步需要进程通信

 

 

 

管道
Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道,管道分为「匿名管道」和「命名管道」。

不管是匿名管道还是命名管道,通信流程都是:一个进程将数据缓存在内核中,另一个进程从内核中获取数据,同时通信数据都遵循先进先出原则。

匿名管道
shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流,并且大小受限,通信的方式是单向的,数据只能在一个方向上流动。
如果要双向通信,需要创建两个管道
并且匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。

命名管道
突破了匿名管道只能在亲缘关系进程间的通信限制,毫无关系的进程可以进行通信。

消息队列
消息队列克服了管道通信效率低的问题
实现原理:消息队列实际上是保存在内核的「消息链表」中,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。

自身的问题:消息队列通信的开销是很大的,因为每次数据的写入和读取都需要经过用户态与内核态之间的拷贝。

共享内存(虚拟内存)
共享内存可以解决消息队列通信中开销过大的问题。
它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,是最快的进程间通信方式。
但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据冲突。

信号量
信号量在共享内存的基础上,规定进程之间必须互斥访问,以确保任何时刻只能有一个进程访问共享资源,避免共享内存导致数据冲突的问题。

信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。

信号
信号与信号量的名字虽然相似,但功能一点儿都不一样。

上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程

信号事件的来源:

硬件来源(如键盘 Cltr+C )
软件来源(如 kill 命令)

管道(效率低)
是什么?
所谓的管道,就是内核里面的一串缓存。从管道的一端写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。同时通信数据都遵循先进先出原则,另外,管道传输的数据是无格式的流且大小受限。
特点:
管道这种通信方式效率低,不适合进程间频繁地交换数据。
管道的种类:
1、匿名管道
例如:ps auxf | grep mysql,命令行里的「|」竖线就是一个管道,它的功能是将前一个命令(ps auxf)的输出,作为后一个命令(grep mysql)的输入,从这功能描述,可以看出管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行。上面这种管道是没有名字,所以「|」表示的管道称为匿名管道,用完了就销毁。
2、命名管道(FIFO)
因为数据是先进先出的传输方式。

通信范围:

对于匿名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程文件描述符 fd 来达到通信的目的。
对于命名管道,不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。
不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,

管道原理
匿名管道的创建,需要通过下面这个系统调用:

int pipe(int fd[2])
1
这里表示创建一个匿名管道,并返回了两个描述符,一个是管道的读取端描述符 fd[0],另一个是管道的写入端描述符 fd[1]。注意,这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中。

 

 

 

再看在 shell 里面执行 A | B命令是怎么的?如下:

 

 

 

 

消息队列(效率高但开销大)
前面说到管道的通信方式是效率低的,因此管道不适合进程间频繁地交换数据。
对于这个问题,消息队列的通信模式就可以解决。比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。

特点:
消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体,消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。
如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

生命周期:
消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。

缺点:

一是通信不及时,
二是数据大小有限制,消息队列不适合大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。
开销:
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。

消息队列面对的问题

消息传递系统现在面临着许多信号量和管程所未涉及的问题和设计难点,尤其对那些在网络中不同机器上的通信状况。

例如:

1、消息丢失
2、确认消息丢失
3、身份验证
身份验证也是一个问题,比如客户端怎么知道它是在与一个真正的文件服务器通信,从发送方到接收方的信息有可能被中间人所篡改。

共享内存(效率高开销小但是可能地址冲突)
消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝的资源消耗,开销大。而共享内存的方式就很好的解决了这一问题。

现代操作系统,对于内存管理,采用的是虚拟内存技术,也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样进程A写入的东西,进程B马上就能看到了,不需要拷贝来拷贝去,大大提高了进程间通信的速度。

 

信号量(PV操作)
用了共享内存通信方式可以解决消息队列开销大的问题,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。

信号量在共享内存的基础上,设置进程之间必须互斥访问,达到保护共享资源,以确保任何时刻只能有一个进程访问共享资源,避免共享内存导致数据冲突的问题。

信号量就是一个整型计数器,记录资源的数量,主要用于实现进程间的互斥与同步。

PV操作:控制信号量的两种原子操作

P操作是用在进入共享资源之前对资源数-1
V 操作是用在离开共享资源之后对资源数+1
这两个操作是必须成对出现的。

一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。

另一个是 V 操作,这个操作会把信号量加上 +1,相加后如果信号量 <= 0,则表明当前仍有阻塞中的进程,于是会挑一个阻塞进程将其唤醒;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;

信号(以上都是同步通信,信号是异步)
上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程

信号事件的来源:

硬件来源(如键盘 Cltr+C )
软件来源(如 kill 命令)
在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:

 

 

运行在 shell 终端的进程,我们可以通过键盘输入某些组合键的时候,给进程发送信号。例如

Ctrl+C 产生 SIGINT 信号,表示终止该进程;

Ctrl+Z 产生 SIGTSTP 信号,表示停止该进程,但还未结束;

如果进程在后台运行,可以通过 kill 命令的方式给进程发送信号,但前提需要知道进程 PID 号
例如:kill -9 1050 ,表示给 PID 为 1050 的进程发送 SIGKILL 信号

 

posted on 2023-02-03 10:34  冬枭  阅读(61)  评论(0编辑  收藏  举报