网络编程套接字

网络编程套接字

文章目录

  • 网络编程套接字
    • 预备知识
      • 理解源IP地址和目的IP地址
      • 认识端口号
      • 理解“端口号”和"进程ID"
      • 理解源端口号和目的端口号
      • 认识TCP协议
      • 认识UDP协议
      • 网络字节序
        • 网络字节序的规则
        • htonl和ntohl的大小端转换
    • socket编程接口
      • socket常见API理解
        • socket
        • bind
        • inet_addr和inet_ntoa(点分十进制的转换)
        • recvfrom(udp)
        • sendto(udp)
        • INADDR_ANY
        • listen(tcp)
        • accpet(tcp)
        • recv(tcp)
        • send(tcp)
        • connect(tcp)
      • sockaddr结构
      • udp代码实现及细节
        • 原始版本
        • 改进思路
        • 实现简单英译汉词典
      • tcp代码实现及细节
        • 单进程版本
        • 多进程版本
        • 多线程版本
        • 线程池版本
        • 小结
      • linux下查看网络状态
        • netstat -nltp
    • TCP协议通讯流程
      • 概念
      • 三次握手的概念及四次挥手的概念和接口的对应
      • 简单理解三次握手和四次挥手
    • TCP和UDP的对比
    • 文件描述符和socket理解

预备知识

我们编写的程序为用户层程序,要用的接口都是系统调用接口,可以理解为在用户层自定义协议,其在应用层,使用的是传输层的接口。后续学习的一般为传输层接口。

理解源IP地址和目的IP地址

在IP数据包头部中,有两个IP地址,分别叫做源IP地址,和目的IP地址。

跨网络传输的第一件工作就是保证准确的找到目标主机。IP标定全公网内唯一一台主机。目的IP保证数据往哪里发。除了把请求交给远端服务器外,也要让远端服务器能找到自己。

电脑打开浏览器,硬件是承担通信的载体,实际通信的是手机上搭载的软件和服务器软件,浏览器本身是一个硬盘上的程序,运行成为进程,所谓套接字本质就是进程间通信。

打开手机虽然服务端看不到,要想使用微信得先打开微信客户端,打开后从系统层面来说就是进程,在远端也有一个对等的服务器(软件层面人编写的服务器)来提供服务。

一台电脑上也不止一个进程同时在跑,我们的IP地址主要负责的是把数据从一台主机硬件传送到另一台主机硬件,如何确定服务器主机获得的数据包给哪个进程?

因此还要通过某种方式标识主机上的特定进程。端口号用来标识特定一台主机上的唯一一个进程。

客户端有IP地址,服务端有IP地址,客户端特定一个进程有端口号,服务端特定一个进程也有端口号。

IP+PORT(端口号)标识的是全网内唯一一个进程,通常将IP+端口号叫做套接字。socket套接字本质是进程间通信。

网络编程套接字_第1张图片

认识端口号

端口号(port)是传输层协议的内容。

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
    • 收到数据一定是硬件先收到的,应用层在最顶层,所以必须贯穿操作系统,操作系统是软硬件的管理者,一定能通过某种方式把数据从底层交给应用层。
  • IP地址+端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用。
    • 端口号是用来唯一标识用途的,一个端口号只能被一个进程绑定,而一个进程可以有多个端口号。进程:端口号=1:n。

理解“端口号”和"进程ID"

之前在学习系统编程的时候,pid表示系统内唯一一个进程;此处我们的端口号也是唯一表示一个进程。那么这两者之间是怎样的关系?

不是所有进程都是网络进程,因此不是所有进程都需要端口号,但是所有进程在系统层面被管理都需要唯一编号。端口号标定在众多网络进程中要找的那个进程。

PID是每个进程都要有,当该进程是网络进程需要指派端口号。

因此一个进程可以既有PID又有端口号,也可以只有PID没有端口号。

另外,一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定。

举个例子:10086的人工客服接听时说的工号就是端口号,该客服自身的身份证是进程ID。

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号,就是在描述"数据是谁发的, 要发给谁"。"数据是谁发的"是目的主机返回数据使用的。

光光找到IP地址对应的主机没有用,还要找到对应接收的进程。

认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;

后面我们再详细讨论TCP的一些细节问题:传输层协议、有连接、可靠传输、面向字节流。

TCP协议保证可靠性,丢包,对方来不及接收,网络拥塞等问题,导致TCP协议比较复杂,没有其他协议那么高效。

网络编程套接字_第2张图片

认识UDP协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;

后面再详细讨论:传输层协议、无连接、不可靠传输、面向数据报。

只负责将报头往下塞,设计比较简单,往往会比较高效一些。

网络编程套接字_第3张图片

网络字节序

网络字节序的规则

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?

规定默认网络中的数据是大端的。

大小端的判别:小小小——数据有高权值位和低权值位之分,内存有高地址和低地址之别,低权值的数据放在低地址称为小端。

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 低地址可能存的是高权值数据,也可能存的是低权值数据。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
    • 小端机器:0x12345678,权值位高的12,先转成大端:0x78563412,先发送低地址的,就是先发送的是12,再发34,56,78。
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略,直接发送即可;接收同理。

为什么网络要采用大端序列

比如说要发一个字符串"abcdef",到对端之后想从开始位置进行计算,采用大端传送可以边接收边计算。

htonl和ntohl的大小端转换

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

htonl:host-to-net-long。从主机序列转成网络序列

#include 
uint32_t htonl(unint32_t hostlong);
uint16_t htons(unint16_t hostshort);
uint32_t ntohl(unint32_t netlong);
uint16_t ntohs(unint16_t netshort);

其中端口号是16位。

IP地址:“XXX.XXX.XXX.XXX”,网络中尽可能用较少数据传输我们的内容,每个区域(0~255)只占1个字节,4个区域要用四个字节。

00000000 00000000 00000000 00000000。IP传输的时候不用传.。因此使用32_t进行转化IP地址。

  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

socket编程接口

socket常见API理解

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int sockfd, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int sockfd, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int sockfd, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

socket

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

参数 含义
domain(域) 表明使用的协议族中的某个协议,常见为AF_INET表明底层实现为IPV4。
type 表示创建套接字的类别,流式(udp)[SOCK_DGRAM],字节流(tcp)[SOCK_STREAM],原始套接字.
protocol 采用的协议,默认设置为0。
返回值 sockfd返回值是文件描述符,linux中一切皆文件,实现网络通信要打开网卡设备及创建对应的数据结构等,网卡是文件,通信前得先打开网卡,和网卡关联,以文件描述符打开文件。编译程序,运行进程,进程运行起来执行socket代码,创建struct file,内部内容指向网络相关信息。 打开文件open进行读写,这里的socket就相当于之前系统调用open,将网络信息当作文件方式打开。 image-20220205083543919

进程间通信是需要通信资源的,最重要的一个资源是网卡。linux下一切皆文件,那么网卡要和文件系统相关挂接。

网卡,硬盘,显示器,键盘是硬件外围设备,操作系统要进行管理,先描述再组织,linux内核中包含struct device去描述所有设备,网络设备一般为net device

创建套接字的过程目前来说就是在底层创建通信需要的网卡相关的资源,第二个是需要和文件系统挂钩的文件系统相关资源。

一个进程可以打开一个套接字,可以和打开文件类似打开多个套接字,系统中如果存在多个套接字,操作系统就要先描述再组织进行管理,struct socket。我们重点关心和文件怎么关联。

进程和套接字怎么产生关联?通过文件描述符表关联起来。

进程启动,得到文件描述符,通过文件描述符找到对应的files_structfiles_struct中有一个void *private_data的字段,指向socket结构,至此,完成对应的通信。socket中有proto_ops,其内部存函数指针,将来不同的套接字种类指向不同的套接字方法,这叫多态。

struct socket
{
     socket_state  state; // socket state
      
     short   type ; // socket type
      
     unsigned long  flags; // socket flags
      
     struct fasync_struct  *fasync_list;
      
     wait_queue_head_t wait;
      
     struct file *file;
      
     struct sock *sock;  // socket在网络层的表示;
      
     const struct proto_ops *ops;/*操作指针,里面都是函数指针*/
           
}
 
 
struct socket结构体的类型
enum sock_type
{
   SOCK_STREAM = 1, // 用于与TCP层中的tcp协议数据的struct socket
    
   SOCK_DGRAM  = 2, //用于与TCP层中的udp协议数据的struct socket
    
   SOCK_RAW    = 3, // raw struct socket
    
   SOCK_RDM    = 4, //可靠传输消息的struct socket
    
   SOCK_SEQPACKET = 5,// sequential packet socket
    
   SOCK_DCCP   = 6,
    
   SOCK_PACKET = 10, //从dev level中获取数据包的socket
};

bind

int bind(int sockfd, const struct sockaddr *address,socklen_t address_len);

之前打开的普通文件关联的是硬盘中的某个文件,绑定本质是把内存文件和网络信息关联起来。将Server自己的ip地址和端口号绑定起来,方便其他用户找到。在系统层面来说,文件信息有了,网络信息有了,绑定是将两者产生关系。

服务器需要绑定,客户端不需要绑定。

参数 含义
返回值 表示绑定成功与否
int sockfd 套接字
const struct sockaddr* address 给套接字绑定的ip+端口,初始化底层函数指针指向不同的标准方法
socklen_t address_len 传入的长度

注意直接写的ip和端口号是用户层的,实际上套接字要在内核层创建用来,绑定也是要内核层的。要把接口参数继续往下传。

讲一切皆文件的时候,学习了文件结构体里包含了大量的函数指针。绑定主要做两件事:填充当前服务器的ip地址和端口号;不同套接字的操作方法不一样,初始化底层的函数指针,指向不同的标准方法。

服务端要bind,而客户端都不需要bind,但是需要IP和port,原因是什么?

为什么服务器要bind?

服务器要强制把端口占住。服务器也会出现冲突,但是都是公司内部,可以协商,而且有利于将服务和端口形成强相关,比如http:80。服务器要一对多,因此其端口必须众所周知,明确的。

而客户端可以进行bind,但是不需要。因为一般server端的ip和port不能轻易更改,必须是确定的,众所周知的。比如http:80,https:443,ssh:22,MySQL:3306。服务器和客户端的关系是1对n,一旦修改了客户端就找不到了。

并且客户端都不需要bind,一个客户来说有多种客户端并且会同时跑,因此如果绑定那么不同的公司就要互相协商规定各自使用什么端口。如果要绑定一个端口号,而这个端口号被另一个客户端给绑定了,绑定就会出错。因为一个端口号只能和一个进程相关。

总的来说,客户端都不需要bind:

  1. 在进行bind的时候,很容易冲突,客户端无法启动
  2. 客户端需要唯一性,但是不需要明确必须是哪个端口号

但是需要IP和port:client udp,recv和send,系统会自动进行ip和端口号的绑定,操作系统熟悉端口号的使用情况,就类似文件描述符的分配,由操作系统进行管理

inet_addr和inet_ntoa(点分十进制的转换)

inet_addr:核心工作将字符串分割的ip地址转化成四字节的网络序列IP。

#include 
#include 
#include 

int inet_aton(const char *cp, struct in_addr *inp);

in_addr_t inet_addr(const char *cp);

in_addr_t inet_network(const char *cp);

char *inet_ntoa(struct in_addr in);

struct in_addr inet_makeaddr(int net, int host);

in_addr_t inet_lnaof(struct in_addr in);

in_addr_t inet_netof(struct in_addr in);

char *inet_ntoa(struct in_addr in):把四个字节的IP地址转换成点分十进制IP,同时变成网络序列。

  • 模拟点分十进制的转换
union{
    unsigned int ip;
    typedef struct {
       unsigned int part1:8;
       unsigned int part2:8;
       unsigned int part3:8;
       unsigned int part4:8;
    }ip_seg;/*位段*/
}ip_t;
int main()
{
    ip_t Ip;
    Ip.ip =12345;
    /*数字转换成字符串:分别获得四个部分,带上.进行转化成char*/
    ip.ip_seg.part1,part2,part3,part4
    /*字符串转化成数字:把四个字符分别转化成整数,直接读取ip值即可*/
        
}

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。那么是否需要调用者手动释放呢?

man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。

那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码

#include
#include
#include

using namespace std;

int main()
{
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;

    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;

    char* ptr1 = inet_ntoa(addr1.sin_addr);
    char* ptr2 = inet_ntoa(addr2.sin_addr);

    cout<<ptr1<<" "<<ptr2<<endl;
}

image-20220207095042902

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。

思考:

  • 如果有多个线程调用inet_ntoa, 是否会出现异常情况呢?
  • 在APUE中, 明确提出inet_ntoa不是线程安全的函数; 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题

测试代码:

#include 
#include 
#include 
#include 
#include 
#include 
void* Func1(void* p) {
     struct sockaddr_in* addr = (struct sockaddr_in*)p;
     while (1) {
       char* ptr = inet_ntoa(addr->sin_addr);
       printf("addr1: %s\n", ptr);
       sleep(1);
     }
     return NULL;
}
void* Func2(void* p) {
     struct sockaddr_in* addr = (struct sockaddr_in*)p;
     while (1) {
       char* ptr = inet_ntoa(addr->sin_addr);
       printf("addr2:%s\n",ptr);
       sleep(1);
     }
     return NULL;
}

int main()
{
      pthread_t tid1 = 0;
      struct sockaddr_in addr1;
      struct sockaddr_in addr2;
      addr1.sin_addr.s_addr = 0;
      addr2.sin_addr.s_addr = 0xffffffff;
      pthread_create(&tid1, NULL, Func1, &addr1);
      pthread_t tid2 = 0;
      pthread_create(&tid2, NULL, Func2, &addr2);
      pthread_join(tid1, NULL);
      pthread_join(tid2, NULL);
      return 0; 
}

image-20220207100013880

recvfrom(udp)

recvfrom:收数据,类似普通文件的read

flag:表示等待的状态,可以阻塞(0)等或者非阻塞。

src_addr:发送者,方便发回去。

addrlen:发送的信息长度。

ssize_t:-1表示错误,其他表示收到的数据字节大小。ssize_t(int),size_t(unsigned int)

socklen_t *addr:既做输入又做输出,输入的时候表示传入的结构体大小,输出表示实际收到的结构体大小。

#include 
#include 

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

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

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

sendto(udp)

sendto:发消息

#include 
#include 

ssize_t send(int sockfd, const 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 sendmsg(int sockfd, const struct msghdr *msg, int flags);

INADDR_ANY

实际真正在工程中写入的时候服务器不需要ip。也就是说在构造的时候只要传一个端口号。

网络地址为INADDR_ANY,这个宏表示本地(server)的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。

如果是IP地址是一个具体的IP地址,那么只有从这个IP地址上上来的报文才会交付。

网络编程套接字_第4张图片

网络编程套接字_第5张图片

网络编程套接字_第6张图片

云服务器可能因为安全问题没法在公网IP开启对应的端口号。

网络编程套接字_第7张图片

listen(tcp)

举个例子,平时去下馆子正常时间都能吃,是因为有人一直在等,这个等就是处于监听状态。

listen将套接字状态设置为监听状态,允许任何时刻有客户端来连接。tcp不连接不能直接发送数据。udp可以。

#include        
#include 

int listen(int sockfd, int backlog);

backlog之后会再理解。

backlog的理解先举个例子,比如有一家饭馆生意特别好,去吃的时候人已经满了,如果非要等那就等着,如果等着的人多了就要排队。因此backlog表示底层连接来到之后但是该连接无法被立即处理时底层的连接队列的长度是多少。这叫全连接队列。还有半连接队列,和三次握手有关。

这个值一般不要设太大。

accpet(tcp)

socket:创建的套接字

address:来连接的客户端的信息

后两个参数都是输出型参数,作用和udp的一样。

返回值int是一个文件描述符。因此如果来10个连接请求,就会有11个连接请求。一个是之前申请的。

那么这里的这个返回值又和之前的有什么区别吗?

举个例子:美食小吃一条街上每家店门口都有一个拉客的,当把客人拉进来找到服务员招待客人之后,拉客的又接着去拉客人。拉客的和服务员都是一家店的。这里的sockfd就是拉客(专注于从底层获取链接上来,是server的listen socket)的,而返回值就是用来对用户进行服务的(专注于进行通信)。张三会被很多拉客的人拉,如果张三拒绝了拉客的就会拉其他客人。

int accept(int sockfd, struct sockaddr* address,socklen_t* address_len);

帮助获取到对应的连接端的IP和端口号相关的信息。

recv(tcp)

一般的文件,pipe——stream

socket,tcp——stream

read,write,recv,send。

read,write不能用于udp。

更建议用recv,比read功能更多。

recv:

flag:默认为0,表示阻塞。

返回值:正数表示读到的字节数,-1表示出错,0表示对端被关闭了。

#include 
#include 

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

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

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

read:

#include 

ssize_t read(int fd, void *buf, size_t count);//count表示希望读到的字节,ssize_t表示实际收到的字节

send(tcp)

更建议使用send,比read功能更多。

send:

#include 
#include 

ssize_t send(int sockfd, const 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 sendmsg(int sockfd, const struct msghdr *msg, int flags);

write:

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

connect(tcp)

sockfd:连接成功客户端通过创建的套接字(参数里的sockfd)向服务器发起通信。使用新socket是服务器的工作。

addr和addrlen:和sendto接口一样,连接服务器,指明连接的具体进程。

返回值:标识连接成功与否。如果为0则成功。

#include           /* See NOTES */
#include 

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

用于客户端

sockaddr结构

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

从上述的代码实例中可以看出,sockaddr是统一的接口,只用一个接口完成不同套接字之间的通信问题。

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。

设计成统一的目的是为了设计尽量少的接口。

网络编程套接字_第8张图片

共同点是三者的首地址都是存的地址类型。

网络编程套接字_第9张图片

达到同一套接口,传入参数的不同,进行不同的函数操作,达到了静态多态——函数重载。(该思想在讲述之前的“一切皆文件”的时候也提及了)

网络编程套接字_第10张图片

可以发现,所有的接口都是struct sockaddr类型,如何区分对于具体的传入使用哪个结构体。提取第一个字段进行if判断,如果为AF_INET则为struct sockaddr_in。(全大写的是宏)

网络编程套接字_第11张图片

网络编程套接字_第12张图片

网络编程套接字_第13张图片

image-20220205122426816

udp代码实现及细节

原始版本

udpServer.hpp

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include

class udpServer{
    private:
      std::string ip;
      int port;
      int sock;
    public:
      udpServer(std::string _ip="127.0.0.1",int _port = 8080)
        :ip(_ip),
        port(_port)
      {}
      void initServer()
      {
          
          sock = socket(AF_INET,SOCK_DGRAM,0); /*在系统资源上申请了一批网络通信需要的资源*/
          std::cout<<sock<<std::endl;
          /*把网络通信相关的数据和资源关联起来*/
          struct sockaddr_in local;
          local.sin_family = AF_INET;
          local.sin_port = htons(port);
          local.sin_addr.s_addr = inet_addr(ip.c_str());

          if( bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0 )
          {
              std::cerr << "bind error" << std::endl;
              exit(1);
          }
          
      }
      /*echo server*/
      void start()
      {
          char msg[64];/*udp面向数据块*/
          for(; ;)
          {
              msg[0]='\0';
              struct sockaddr_in end_point;
              socklen_t len = sizeof(end_point);
              ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,\
                  0,(struct sockaddr*)&end_point,&len);
              if( s > 0 )
              {
                 msg[s]= '\0';
                 std::cout << "cliet# "<<msg<<std::endl;
                 std::string echo_string = msg;
                 echo_string += " [ severr echo! ]";
                
                 sendto(sock,echo_string.c_str(), echo_string.size(),0,\
                      (struct sockaddr*)&end_point , len
                     );
              }
          }
      }
      ~udpServer()
      {
          close(sock);
      }
};

udpClient.hpp

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include

class udpClient{
    private:
      std::string ip;
      int port;
      int sock;
    public:
      //ip,port? server's ip,port!!
      udpClient(std::string _ip="127.0.0.1",int _port = 8080)
        :ip(_ip),
        port(_port)
      {}
      void initServer()
      {
          sock = socket(AF_INET,SOCK_DGRAM,0); 
          std:: cout<<sock<<std::endl;
      }
      /*echo server*/
      void start()
      {
         /*明确客户端发送报文给谁发*/
         std::string msg;
         struct sockaddr_in peer;
         peer.sin_family = AF_INET;
         peer.sin_port = htons(port);
         peer.sin_addr.s_addr = inet_addr(ip.c_str());

          for(; ;)
          {
            std::cout<<"Please Enter# ";
            std::cin >>msg;
            if(msg=="quit")
            {
              break;
            }
            sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));

            char echo[128];
            ssize_t s = recvfrom(sock,echo,sizeof(echo)-1,0,nullptr,nullptr);/*可以nullptr也可以传具体的*/
    
            if(s>0)
            {
              echo[s] = 0;
              std::cout<<"server# "<<echo<<std::endl;
            }
          }
      }
      ~udpClient()
      {
          close(sock);
      }
};

改进思路

  1. IP地址不再需要显式给定,使用INADDR_ANY宏
  2. 简单的群聊功能,server可以通过sockaddr获得(结构体中含有IP和port),存放所有的end_point,当有人给server发消息时,给所有人转发。能拿到对应的IP就能对访问进行控制。比如说恶意攻击禁止IP。
  3. 发来的消息可以是命令,对于发来的消息可以fork子进程进行exec执行命令。可以通过客户端让服务器做事情。对于处理的结果,客户端是文件,是文件就可以重定向(dup2 1 socket),返回结果。

udpServer.hpp

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include

class udpServer{
    private:
     // std::string ip;
      int port;
      int sock;
    public:
      udpServer(int _port = 8080)
        :
        port(_port)
      {}
      void initServer()
      {
          sock = socket(AF_INET,SOCK_DGRAM,0); 
          std:: cout<<sock<<std::endl;
          struct sockaddr_in local;
          local.sin_family = AF_INET;
          local.sin_port = htons(port);
          //local.sin_addr.s_addr = inet_addr(ip.c_str());
          local.sin_addr.s_addr = INADDR_ANY;

          if( bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0 )
          {
              std::cerr << "bind error" << std::endl;
              exit(1);
          }
          
      }
      /*echo server*/
      void start()
      {
          char msg[64];
          for(; ;)
          {
              msg[0]='\0';
              struct sockaddr_in end_point;
              socklen_t len = sizeof(end_point);
              ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,\
                  0,(struct sockaddr*)&end_point,&len);
              if( s > 0 )
              {
                 /*也可以to_string*/
                 char buf[32];
                 sprintf(buf,"%d",ntohs(end_point.sin_port));
                  
                 std::string cli = inet_ntoa(end_point.sin_addr);
                 cli +=":";
                 cli +=buf;

                 msg[s]= '\0';
                 
                 std::cout<<cli <<"#"<<msg<<std::endl;

                 std::string echo_string = msg;
                 echo_string += " [ severr echo! ]";
                
                 sendto(sock,echo_string.c_str(), echo_string.size(),0,\
                      (struct sockaddr*)&end_point , len
                     );
              }
          }
      }
      ~udpServer()
      {
          close(sock);
      }
};

updServer.cc

#include"udpServer.hpp"

void Usage(std::string proc)
{
  std::cout <<"Usage:"<<proc <<" local_port"<<std::endl;
}
//./udpServer ip port
int main(int argc,char *argv[])
{
    if(argc != 2){
       Usage(argv[0]);
       exit(1);
    }
    udpServer *up = new udpServer(atoi(argv[1]));
    up->initServer();  
    up->start();
    delete up;
}

实现简单英译汉词典

这个过程其实就一个制定应用层协议的过程。

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

class udpServer{
    private:
     // std::string ip;
      int port;
      int sock;
      std::map<std::string,std::string> dict;
    public:
      udpServer(int _port = 8080)
        :
        port(_port)
  {
       dict.insert(std::make_pair("apple","苹果")); 
       dict.insert(std::make_pair("banana","香蕉"));
       dict.insert(std::make_pair("student","学生"));
  }
      void initServer()
      {
          sock = socket(AF_INET,SOCK_DGRAM,0); 
          std:: cout<<sock<<std::endl;
          struct sockaddr_in local;
          local.sin_family = AF_INET;
          local.sin_port = htons(port);
          //local.sin_addr.s_addr = inet_addr(ip.c_str());
          //local.sin_addr.s_addr = INADDR_ANY;
          local.sin_addr.s_addr = 0;
          if( bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0 )
          {
              std::cerr << "bind error" << std::endl;
              exit(1);
          }
          
      }
      /*echo server*/
      void start()
      {
          char msg[64];
          for(; ;)
          {
              msg[0]='\0';
              struct sockaddr_in end_point;
              socklen_t len = sizeof(end_point);
              ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,\
                  0,(struct sockaddr*)&end_point,&len);
              if( s > 0 )
              {
                 /*也可以to_string*/
                 char buf[32];
                 sprintf(buf,"%d",ntohs(end_point.sin_port));
                  
                 std::string cli = inet_ntoa(end_point.sin_addr);
                 cli +=":";
                 cli +=buf;

                 msg[s]= '\0';
                 
                 std::cout<<cli <<"#"<<msg<<std::endl;
        
                 std::string echo = "unknown";
                 auto ret = dict.find(msg);
                 if(ret != dict.end() )
                 {
                    echo = dict[msg]; 
                 }
                 //echo_string += " [ severr echo! ]";
                 sendto(sock,echo.c_str(), echo.size(),0,\
                      (struct sockaddr*)&end_point , len
                     );
              }
          }
      }
      ~udpServer()
      {
          close(sock);
      }
};

tcp代码实现及细节

单进程版本

tcpServer.hpp:

#ifndef __TCP_SERVER_H
#define __TCP_SERVER_H

#include
#include
#include
#include
#include
#include
#include
#include

#define BACKLOG 5

#endif

class tcpServer{
    private:
        int port;
        int listensock;//监听套接字
    public:
        tcpServer(int _port)
          :port(_port),
          listensock(-1)
        { }
        void initServer()
        {
            /*创建套接字信息,文件相关信息*/
            listensock = socket(AF_INET,SOCK_STREAM,0);
            if(listensock < 0 )
            {
                std::cerr <<"listensocket error" <<std::endl;
                exit(2);
            }
            /*用户层填充套接字信息*/
            struct sockaddr_in local;
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = htonl(INADDR_ANY);
			/*将文件信息和网络信息进行绑定*/
            if(bind(listensock , (struct sockaddr*)&local, sizeof(local))<0 ){
                std::cerr << "bind error" << std::endl;
                exit(3);
            } 
            /*将套接字设置为监听状态*/
            if( listen(listensock ,BACKLOG ) <0 )
            {
               std::cerr <<"listen error" <<std::endl;
               exit(4);
            }
        }
        //BUG
        void service(int sock)
        {
            char buffer[1024];
            while(true)
            {
              //read or write ->ok 
              size_t s = recv(sock, buffer,sizeof(buffer) -1,0);
              if( s > 0 )
              {
                buffer[s] = 0;
                std::cout<<"client# "<<buffer<<std::endl;

                send(sock,buffer ,strlen(buffer),0 ); /*网络是文件,'\0'是C语言的不是文件的*/
              }

            }
        }
        void start()
        {
            sockaddr_in end_point;//远端
            while(true)/*进程常驻内存*/
            {
               socklen_t len = sizeof(end_point);
               int sock = accept(listensock,(struct sockaddr*)&end_point,&len);/*多个底层链接在应用层连接拿上来*/
               if( sock < 0 )
               {
                  std::cerr<<"accept error"<<std::endl;
                  continue;
               }
               /*通信*/
              std::cout<< "get a new link..." <<std::endl;
              service(sock); //进行IO服务
            }
        }
        ~tcpServer()
        { }
};

代码效果:

网络编程套接字_第14张图片

网络编程套接字_第15张图片

网络编程套接字_第16张图片

网络编程套接字_第17张图片

tcpClient.hpp:

#ifndef __TCP_CLIENT_H
#define __TCP_CLIENT_H

#include
#include
#include
#include
#include
#include
#include
#include
#include

#endif

class tcpClient{
      private:
        std::string svr_ip;
        int svr_port;
        int sock;
      public:
        tcpClient(std::string _ip="127.0.0.1" ,int _port = 8080)
          :svr_ip(_ip),
          svr_port(_port)
        {}
        void initClient()
        {
            sock = socket(AF_INET, SOCK_STREAM , 0);
            if(sock < 0)
            {
               std::cerr<<"socket  error"<<std::endl;
               exit(2);
            }
            //bind?客户端不需要绑定,也不需要监听,因为没人连,也不需要accept,因为没有listen socket
            struct sockaddr_in svr;
            svr.sin_family = AF_INET;
            svr.sin_port = htons(svr_port);
            svr.sin_addr.s_addr = inet_addr( svr_ip.c_str() ) ;
            if( connect(sock,(struct sockaddr*)&svr , sizeof(svr)) != 0 )
            {
                std::cerr<<"connect error" <<std::endl;
            }
            //connect success
        }
        void start()
        {
            char msg[64];
            while(true)
            {
               size_t s = read(0,msg ,sizeof(msg)-1);
               if( s>0 )
               {
                  msg[s]=0;
                  send(sock,msg,strlen(msg),0);//文件发送不用'\0'
                  size_t ss = recv(sock,msg,sizeof(msg)-1,0);
                  if(ss > 0 )
                  {
                        msg[ss] = 0;
                        std::cout<<"server echo $" <<msg <<std::endl;
                  }
               }
            }
        }
        ~tcpClient()
        {
            close(sock);
        }
};

当关掉服务端之后客户端未关闭时服务端没法再启动

网络编程套接字_第18张图片

当客户端退出时:

  1. 服务器要知道客户端退出
  2. 让服务器从while循环退出
  3. 让服务器关闭给新来链接打开的套接字。

在处理的时候通过recv\read返回值为0时判断客户端是否退出了。可以发现写端退出读端关闭,tcp通信在细节上和管道特别像,两者都是流式,管道是以本地的文件作为通信双方的临界资源,现在临界资源是套接字或者网络。

网络编程套接字_第19张图片

简单回顾下前台进程转后台进程(&运行或者ctrl+z停止),jobs查看后台进程,以及bg1(让后台进程运行)或fg1将后台进程切换回前台。注意当进程中含有需要前台输入部分功能bg1没法唤醒停止的后台进程。(将client中的读取部分注释即可演示)

网络编程套接字_第20张图片

网络编程套接字_第21张图片

tcp.server.h

#ifndef __TCP_SERVER_H
#define __TCP_SERVER_H

#include
#include
#include
#include
#include
#include
#include
#include
#include

#define BACKLOG 5

#endif

class tcpServer{
    private:
        int port;
        int listensock;//监听套接字
    public:
        tcpServer(int _port)
          :port(_port),
          listensock(-1)
        { }
        void initServer()
        {
            listensock = socket(AF_INET,SOCK_STREAM,0);
            if(listensock < 0 )
            {
                std::cerr <<"listensocket error" <<std::endl;
                exit(2);
            }
            struct sockaddr_in local;
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = htonl(INADDR_ANY);

            if(bind(listensock , (struct sockaddr*)&local, sizeof(local))<0 ){
                std::cerr << "bind error" << std::endl;
                exit(3);
            } 

            if( listen(listensock ,BACKLOG ) <0 )
            {
               std::cerr <<"listen error" <<std::endl;
               exit(4);
            }
        }
        void service(int sock)
        {
            char buffer[1024];
            while(true)
            {
              //read or write ->ok 
              size_t s = recv(sock, buffer,sizeof(buffer) -1,0);
              if( s > 0 )
              {
                buffer[s] = 0;
                std::cout<<"client# "<<buffer<<std::endl;

                send(sock,buffer ,strlen(buffer),0 ); /*网络是文件,'\0'是C语言的不是文件的*/
              }
              else if( s== 0 )
              {
                 std::cout <<"client close"<<std::endl;
                 close(sock);
                 break;
              }
              else{
                 std::cout<<"recv client data eroor.."<<std::endl;
                 close(sock);
                 break;
              }
            }
        }
        void start()
        {
            sockaddr_in end_point;//远端
            while(true)
            {
               socklen_t len = sizeof(end_point);
               int sock = accept(listensock,(struct sockaddr*)&end_point,&len); 
               if( sock < 0 )
               {
                  std::cerr<<"accept error"<<std::endl;
                  continue;
               }
               /*通信*/
              std::cout<< "get a new link..." <<std::endl;
              service(sock); 
            }
        }
        ~tcpServer()
        { }
};

tcpclient.hpp

#ifndef __TCP_CLIENT_H
#define __TCP_CLIENT_H

#include
#include
#include
#include
#include
#include
#include
#include
#include

#endif

class tcpClient{
      private:
        std::string svr_ip;
        int svr_port;
        int sock;
      public:
        tcpClient(std::string _ip="127.0.0.1" ,int _port = 8080)
          :svr_ip(_ip),
          svr_port(_port)
        {}
        void initClient()
        {
            sock = socket(AF_INET, SOCK_STREAM , 0);
            if(sock < 0)
            {
               std::cerr<<"socket  error"<<std::endl;
               exit(2);
            }
            //bind?客户端不需要绑定,也不需要监听,因为没人连,也不需要accept,因为没有listen socket
            struct sockaddr_in svr;
            svr.sin_family = AF_INET;
            svr.sin_port = htons(svr_port);
            svr.sin_addr.s_addr = inet_addr( svr_ip.c_str() ) ;
            if( connect(sock,(struct sockaddr*)&svr , sizeof(svr)) != 0 )
            {
                std::cerr<<"connect error" <<std::endl;
            }
            //connect success
        }
        void start()
        {
            char msg[64];
            while(true)
            {
               std::cout<<"Please Enter Message #";
               std::endl;
               size_t s = read(0,msg ,sizeof(msg)-1);
               if( s>0 )
               {
                  msg[s-1]=0; //去掉换行符,read不吸收
                  send(sock,msg,strlen(msg),0);//文件发送不用'\0'
                  size_t ss = recv(sock,msg,sizeof(msg)-1,0);
                  if(ss > 0 )
                  {
                        msg[ss] = 0;
                        std::cout<<"server echo $" <<msg <<std::endl;
                  }
               }
            }
        }
        ~tcpClient()
        {
            close(sock);
        }
};

多进程版本

当第二个客户端连接的时候显示连接不上?

因为原来的服务端是一个单执行流,当服务器等待客户端输入的时候没有得到输入导致服务器被挂起。单执行流会导致任何一个任务一旦发生阻塞问题,其他任务都不可能执行。

网络编程套接字_第22张图片

而对于等待子进程的问题要怎么处理?

  • fork()主进程可以非阻塞等待(不推荐);
  • 注册signalCHID信号,子进程退出时给父进程发信号,发信号时回收(麻烦);
  • 子进程退出时向父亲发送singalCHID信号若父进程忽略,子进程退出时系统自动回收子进程。(回顾一下信号部分)signal(SIGCHLD,SIG_IGN);

另外还有一种小trick是子进程再创建进程,并且让子进程退出,父进程等待子进程可以立即完成,对于孙子进程变成孤儿进程被1号进程收养,不用再wait。(严重不推荐,fork()太多了,创建是有成本的)

同时注意父子进程关闭文件描述符的问题。父进程是一定要关闭的,不然会导致文件描述符越用越少。

#ifndef __TCP_SERVER_H
#define __TCP_SERVER_H

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 5

#endif

class tcpServer{
    private:
        int port;
        int listensock;//监听套接字
    public:
        tcpServer(int _port)
          :port(_port),
          listensock(-1)
        { }
        void initServer()
        {
            signal(SIGCHLD,SIG_IGN);

            listensock = socket(AF_INET,SOCK_STREAM,0);
            if(listensock < 0 )
            {
                std::cerr <<"listensocket error" <<std::endl;
                exit(2);
            }
            struct sockaddr_in local;
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = htonl(INADDR_ANY);

            if(bind(listensock , (struct sockaddr*)&local, sizeof(local))<0 ){
                std::cerr << "bind error" << std::endl;
                exit(3);
            } 

            if( listen(listensock ,BACKLOG ) <0 )
            {
               std::cerr <<"listen error" <<std::endl;
               exit(4);
            }
        }
        void service(int sock)
        {
            char buffer[1024];
            while(true)
            {
              //read or write ->ok 
              size_t s = recv(sock, buffer,sizeof(buffer) -1,0);
              if( s > 0 )
              {
                buffer[s] = 0;
                std::cout<<"client# "<<buffer<<std::endl;

                send(sock,buffer ,strlen(buffer),0 ); /*网络是文件,'\0'是C语言的不是文件的*/
              }
              else if( s== 0 )
              {
                 std::cout <<"client close"<<std::endl;
                 close(sock);
                 break;
              }
              else{
                 std::cout<<"recv client data eroor.."<<std::endl;
                 close(sock);
                 break;
              }
            }
        }
        void start()
        {
            sockaddr_in end_point;//远端
            while(true)
            {
               socklen_t len = sizeof(end_point);
               int sock = accept(listensock,(struct sockaddr*)&end_point,&len); 
               if( sock < 0 )
               {
                  std::cerr<<"accept error"<<std::endl;
                  continue;
               }
               std::string cli_info = inet_ntoa(end_point.sin_addr);
               cli_info += ":";
               cli_info += std::to_string(ntohs(end_point.sin_port));
               /*通信*/
               std::cout<< "get a new link..." <<cli_info<<"sock: "<<sock<<std::endl;

               pid_t id = fork();
               if( id == 0 )
               {
                 //子进程关心的是用于IO的sock,因此推荐关闭listensock,子进程是自己的拷贝部分,不影响父进程
                  close(listensock);
                  service(sock); 
                  exit(0);
               }
               //父进程关心用于接客户端的listensock,关闭sock;父进程和子进程是不同的文件描述表
               close(sock);//父进程必须关闭,不断获取新连接导致文件描述符越用越多被子进程继承 
            }
        }
        ~tcpServer()
        { }
};

每个子进程用的文件描述符都是4。

网络编程套接字_第23张图片

在单独服务器,其他主机群的条件下发现,当有客户端连接时网络状态可以检测到tcpServer的增多。因此如果其中两个tcpServer(两个客户端)之间要互相发消息,就是再进行一个进程间通信的逻辑。同时来了一个客户端就创建一个进程,一对客户的时间成本增加,如果客户端达到小型规模数量进程过多服务器就跑不动了。

多线程版本

  • 此时进行ps ajx| head -1 && ps ajx | grep -ER查看发现只有一个进程,使用之前学习的ps -aL查看轻量级进程。

  • 多线程之间共享文件描述符所以不能关闭。

  • 同时注意对新获得的sock要在堆上备份。

#ifndef __TCP_SERVER_H
#define __TCP_SERVER_H

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 5

#endif

class tcpServer{
    private:
        int port;
        int listensock;//监听套接字
    public:
        tcpServer(int _port)
          :port(_port),
          listensock(-1)
        { }
        void initServer()
        {
            signal(SIGCHLD,SIG_IGN);

            listensock = socket(AF_INET,SOCK_STREAM,0);
            if(listensock < 0 )
            {
                std::cerr <<"listensocket error" <<std::endl;
                exit(2);
            }
            struct sockaddr_in local;
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = htonl(INADDR_ANY);

            if(bind(listensock , (struct sockaddr*)&local, sizeof(local))<0 ){
                std::cerr << "bind error" << std::endl;
                exit(3);
            } 

            if( listen(listensock ,BACKLOG ) <0 )
            {
               std::cerr <<"listen error" <<std::endl;
               exit(4);
            }
        }
        static void service(int sock)
        {
            char buffer[1024];
            while(true)
            {
              //read or write ->ok 
              size_t s = recv(sock, buffer,sizeof(buffer) -1,0);
              if( s > 0 )
              {
                buffer[s] = 0;
                std::cout<<"client# "<<buffer<<std::endl;

                send(sock,buffer ,strlen(buffer),0 ); /*网络是文件,'\0'是C语言的不是文件的*/
              }
              else if( s== 0 )
              {
                 std::cout <<"client close"<<std::endl;
                 close(sock);
                 break;
              }
              else{
                 std::cout<<"recv client data eroor.."<<std::endl;
                 close(sock);
                 break;
              }
            }
        }
        static void *serviceRoutine(void *arg)
        {
               pthread_detach(pthread_self());//主线程不等待就可能内存泄漏,而等待又被阻塞,因此进行线程分离
               std::cout<<" create a new thread for IO"<<std::endl;
               int *p = (int*)arg;
               int sock = *arg; 
               service(sock); //service没有用到类内成员函数,因此可以改成static        
               delete p;
        }
        void start()
        {
            sockaddr_in end_point;//远端
            while(true)
            {
               socklen_t len = sizeof(end_point);
               int sock = accept(listensock,(struct sockaddr*)&end_point,&len); 
               if( sock < 0 )
               {
                  std::cerr<<"accept error"<<std::endl;
                  continue;
               }
               std::string cli_info = inet_ntoa(end_point.sin_addr);
               cli_info += ":";
               cli_info += std::to_string(ntohs(end_point.sin_port));
               /*通信*/
               std::cout<< "get a new link..." <<cli_info<<"sock: "<<sock<<std::endl;

               pthread_t tid;
               int * p =new int(sock);
               pthread_create(&tid, nullptr,serviceRoutine,(void*)p);//bug:新线程没有创建完主线程往下继续执行拿了新的sock,导致sock变了。因此要保存一下之前的变量
        
            }
        }
        ~tcpServer()
        { }
};

线程池版本

实现短链接,客户端处理一次业务收到就退出。

查看代码实现

小结

  1. 单进程:不可使用
  2. 多进程版本:健壮性好,比较吃资源,效率低下
  3. 多线程版本:健壮性不强,较吃资源,效率相对较高

大量客户端:系统会存在大量的执行流,执行流之间的切换周期变长,切换有可能成为效率低下的重要原因。

第一种不可使用,后两者只能适用小型应用,比如局域网。

不管多进程多线程,目前都是客户来了再服务,效率低下。

  1. 线程池:提高效率,防止外部恶意请求的冲击

linux下查看网络状态

netstat -nltp

netstat -nltp:n-num,l-list/listen,t-tcp,p-process

netstat -nlup:n-num,l-list/listen,t-tcp,p-process

网络编程套接字_第24张图片

TCP协议通讯流程

概念

一个服务器在一段时间内会收到很多客户端链接,一旦链接了服务器,服务器就需要对外提供多种链接。系统中当链接足够多的时候操作系统就要先描述再组织描述链接。

**所谓的面向连接本质在创建连接成功之后双方都要为了维护该连接在系统层面创建对应的数据结构来保存对应的连接数据。**双方为了维护连接是有成本的,成本体现在创建数据结构及变量要花时间和空间。Udp不用连接,会快一点,吃资源少一点。

一般而言,服务器被动接收连接,客户端主动发起请求,像这种客户端主动发起请求,服务器被动接收连接的模型叫CS模型

而在TCP中,客户端建立连接的方式叫三次握手。断开连接的方式叫四次挥手。

主动建立连接的地方肯定是客户端,但是断开连接可能是客户端也可能是服务器端。这里以客户端主动断开连接为例。

三次握手和四次挥手的详细细节后续再说。

三次握手的过程中创建对应的数据结构,四次挥手把曾经申请的资源释放掉。建立和释放连接要花费系统资源,和客户端还是服务器没关系。服务端和客户端在技术层面上是平等的。

三次握手的概念及四次挥手的概念和接口的对应

具体的细节之后再学习。下面会具体讲socket()fd的关系。

网络编程套接字_第25张图片

connect:客户端向服务器发送一个携带SYN的数据报,其中connect是一次函数调用,为系统调用接口,通信双方主机在客户端发送connect时底层自动进行三次握手,因此connect是触发链接建立的作用。

accept:底层把链接建立好后accpet才会返回。

write:等同于send

发送DATA之后的ACK:保证可靠性,并且双方通信地位对等。

close:结束端向另一端发送FIN,另一端自动ACK。一个close对应两个动作,发FIN和收ACK。

  • 服务器初始化:
    • 调用socket,创建文件描述符;
    • 调用bind,将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了,就会bind失败;
    • 调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备;
    • 调用accecpt,并阻塞,等待客户端连接过来;
  • 建立连接的过程
    • 调用socket,创建文件描述符;
    • 调用connect, 向服务器发起连接请求; connect会发出SYN段并阻塞等待服务器应答; (第一次) 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次) 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段;(第三次)
  • 数据传输的过程
    • 建立连接后,TCP协议提供全双工的通信服务;所谓全双工的意思是,在同一条连接中,同一时刻,通信双方可以同时写数据;相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据;
    • 服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待;
    • 这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答;
    • 服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求;客户端收到后从read()返回, 发送下一条请求,如此循环下去;
  • 断开连接的过程
    • 如果客户端没有更多的请求了,就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
    • 此时服务器收到FIN后,会回应一个ACK, 同时read会返回0 (第二次);
    • read返回之后,服务器就知道客户端关闭了连接,也调close关闭连接,这个时候服务器会向客户端发送一个FIN;(第三次)
    • 客户端收到FIN,再返回一个ACK给服务器; (第四次)

简单理解三次握手和四次挥手

三次握手的例子:

小明:做我女朋友

小红:好,什么时候开始这段感情

小明:就现在

至此两者建立链接。

四次挥手的例子:

小红:我要跟你离婚

此时只有单方面的离婚。断开连接是双方的事情。

小明:好啊

小明:我也要和你离婚

小红:好啊

四次挥手本质是双方都要认可。

TCP和UDP的对比

  • 可靠传输 vs 不可靠传输
    • tcp有重传机制,序号机制,面向连接机制
    • udp不需要做任何可靠性保证
    • 有些场景允许丢包,比如直播;有些场景不允许丢包,比如支付场景
  • 有连接 vs 无连接
    • tcp要建立连接,connect三次握手
    • udp不用建立连接
  • 字节流 vs 数据报
    • udp的recvfrom从操作系统层读取数据报,要不读不到,读到就读完。就比如取快递都是取完整的,没有说取半个说法。比如发送hello world的时候udp就是打包整个发送接收端也是整个hello world。
    • 字节流就类似生活中的自来水,发送abcdef的时候对方可以读全部也可以读一个也以读两个。每家每户想取多少水是随客户的。用户可以发送hello world,也可以发送hello。接收方可以接收hello world也可以只接收h。数据在底层没有边界。边界可以由用户规定协议。

文件描述符和socket理解

切片和多态的概念:socket底层也采用了这种设计方式。

#include
struct Stu{
  	char name[100];
    char sex;
};
struct good_Stu{
    struct Stu base;
    int good1;//可以直接通过强转指针类型访问
};
struct  A{
    int x;
};
struct B{
    struct A obj;
    int y;
};
struct C{
    struct B obj;
    int z;
};
int main(){
    struct C c;
    struct C *p =&c;
    struct B *p1 = (struct B*)&c;
    struct A *p2 = (struct A*)&c;
}

总结一下socket和文件的映射关系:

一个进程创建套接字的时候本质先创建一个struct file得到文件描述符,同时创建struct socket,让两者关联,struct filevoid* private_data指向struct socket,struct socket*file回指向struct file

struct sock中的sock* sk会指向struct tcp_sock,tcp_sock里面是一摞的结构体,就如上面的demo一样。这部分暂时不细谈,按下不表。

网络编程套接字_第26张图片

现在也就理解了创建一个套接字本质上就是打开了一个文件,创建一个文件就是通过void* private_data指向套接字socket。

通过socket的read/write干了什么事情?

read参数传入文件描述符,通过文件描述符找到struct file,再由void *private_data找到socket,通过socket找到tcp_socket/sock,里面有receive_queue,拿走数据。

write同理。

你可能感兴趣的:(#,网络编程,C++,网络,服务器,linux)