当前位置: 首页 > news >正文

TCP Socket编程

最基本的Socket编程

想客户端和服务器能在网络中通信,就得使用 Socket 编程,它可以进行跨主机间通信。在创建Socket时可以选择传输层使用TCP还是UDP。相对于TCP来说,UDP更为简单,下面以TCP为例。

TCP服务端要先建立起来,等待客户端的连接到来,然后建立起连接。

1、服务端首先调用socket()函数,创建套接字,

2、接着调用bind()函数来绑定IP和地址和端口号。

绑定 IP 地址:一台机器是可以有多个网卡的,每个网卡都有对应的IP 地址,只有当绑定了目标网卡时,内核在收到该网卡上的数据包,才会发给我们。

绑定端口:当内核收到 TCP 报文,通过 TCP 头里面的端口号,来找到我们的应用程序,然后把数据传递给对应端口号的程序。

3、调用listen()函数将创建的套接字设为监听状态,刚刚创建的套接字为监听套接字,即这个套接字只是用来监视有没有客户端发起新连接,并不进行真正的通信。

4、服务端进入了监听状态后,通过调用accept()函数,来从内核获取客户端的连接,如果没有客户端连接,则会阻塞等待客户端连接的到来。

相关代码如下:

            // 1.创建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0);if(_listensock < 0){exit(2);}cout << "create socket success" << endl;// 2.绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;int n_bind = bind(_listensock, (struct sockaddr*)&local, sizeof(local));if(n_bind < 0){exit(3);}cout << "bind socket success" << endl;// 3.监听,设置套接字socket状态为监听状态int n_listen = listen(_listensock, 5);if(n_listen < 0){exit(4);}cout << "listen socket success" << endl;

接下来就是TCP客户端:客户端创建socket,然后调用connect()函数发起连接,并且在connect的时候要指明服务器的IP和端口号;当发起connect后,就开始三次握手过程建立连接,成功后会返回一个文件描述符,是和服务端建立好连接的,然后双方就能进行通信了。

相关代码如下:

        void InitClient(){// 1. 创建socket_sock = socket(AF_INET, SOCK_STREAM, 0);if(_sock < 0){exit(2);}// 2. tcp的客户端要不要bind?要的! 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!// 3. 要不要listen?不要!// 4. 要不要accept? 不要!// 5. 要什么呢??要发起链接!}void Start(){// 发起链接,使用connect// 首先要知道要链接的服务端的ip和portstruct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(_serverip.c_str());server.sin_port = htons(_serverport);int n_connect = connect(_sock, (struct sockaddr*)&server, sizeof(server));if(n_connect < 0){cout << "socket connect error" << endl;}else{string message;while(true){cout << "Enter# ";getline(cin, message);write(_sock, message.c_str(), message.size());char buffer[1024];int n = read(_sock, buffer, sizeof(buffer)-1);if(n > 0){//目前我们把读到的数据当成字符串, 截止目前buffer[n] = 0;cout << "Server回显# " << buffer << endl;}else{break; }}}}

在 TCP 连接的过程中,服务器的内核实际上为每个 Socket 维护了两个队列:
一个是尚未完全建立起连接的队列,称为 TCP 半连接队列,这个队列都是没有完成三次握手的连接,此时服务端处于syn_rcvd 的状态;
一个是已经建立连接的队列,称为 TCP 全连接队列,这个队列都是完成了三次握手的连接,此时服务端处于 established 状态;
当 TCP 全连接队列不为空后,服务端的 accept()函数,就会从内核中的 TCP 全连接队列里拿出一个已经完成连接的 socket 返回应用程序,后续数据传输都用这个 socket。

需要注意的是:监听连接到来的socket和真正通信的socket是不同的

连接建立后,客户端和服务端就开始相互传输数据了,双方都可以通过 read()和 write()函数来读写
数据。

上面所描述的TCP Socket是最简单的,基本只能用来一对一通信,其使用的是同步阻塞的方式,当服务端在还没处理完一个客户端的网络 I/0 时,其他客户端是无法与服务端连接的。但是一个服务器只服务一个客户,这样就太浪费资源了,所以要进行改进:

多进程模型:

服务器的主进程负责监听客户的连接,一旦与客户端连接完成,这时当前进程就通过 fork()函数创建一个子进程,实际上就把父进程所有相关的东西都复制一份,包括文件描述符、内存地址空间、执行的代码等。
因为子进程会复制父进程的文件描述符,于是就可以直接使用已连接 Socket和客户端通信了,可以发现,子进程不需要关心监听 Socket,只需要关心已连接 Socket;父进程则相反,将客户服务交给子进程来处理,因此父进程不需要关心已连接 Socket,只需要关心监听 Socket。

这里需要注意的是要回收子进程,否则会造成僵尸进程的问题,最终导致资源泄漏的问题。这种用多个进程来应付多个客户端的方式,当客户端数量很多时,肯定是扛不住的,因为每产生一个进程,必会占据一定的系统资源,而且进程间上下文切换代价也不小,性能会有很大的影响。所以又有了多线程版本:

多线程模型:

在Linux中线程是更加轻量化的进程,是CPU调度的基本单位,并且线程切换相比于进程切换代价更小,性能会更好,当服务器与客户端 TCP 完成连接后,通过 pthread create()函数创建线程,然后将已连接 Socket的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。
如果每来一个连接就创建一个线程,线程运行完后,还得操作系统还得销毁线程,虽说线程切换的上写文开销不大,但是如果频繁创建和销毁线程,系统开销也是不小的。
那么,我们可以使用线程池的方式来避免线程的频繁创建和销毁,所谓的线程池,就是提前创建若干个线程,这样当由新连接建立时,将这个已连接的 Socket 放入到一个队列里,然后线程池里的线程负责从队列中取出已连接 Socket 进行处理。

相关代码如下:

        void Start(){// 初始化线程池并启动线程池ThreadPool<Task>::getInstance()->run();cout << "Thread init success" << endl;// 4.acceptwhile(1){struct sockaddr_in peer;socklen_t len = sizeof(peer);// accept成功返回一个文件描述符,用来和Client通信,而这里的_sock是用来监听链接到来,获取新链接的。int sock = accept(_listensock, (struct sockaddr*)&peer, &len);if(sock < 0){continue;}cout << "accept a new link success, get new sock: " <<  sock << endl;// 5.这里就是一个sock,未来通信我们就用这个sock,面向字节流的,后续全部都是文件操作!// version 1//serviceIO(sock);//close(sock); //对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏// version 2 多进程// pid_t id = fork();// if(id == 0) // 子进程// {//     // 子进程会有自己独立的进程地址空间//     close(_listensock);//     if(fork() > 0) exit(0);//     serviceIO(sock);//     close(sock);//     exit(0);// }// close(sock);// // 父进程// // 子进程结束需要父进程来回收,避免僵尸进程// pid_t ret = waitpid(id, nullptr, 0);// if(ret>0)// {//     std::cout << "waitsuccess: " << ret << std::endl;// }// version 3 多线程// pthread_t tid;// ThreadData* td = new ThreadData(this, sock);// pthread_create(&tid, nullptr, threadRoutine, td);// version 4 线程池ThreadPool<Task>::getInstance()->push(Task(sock, serviceIO));}}

上面的代码是部分代码,具体的代码可以通过下面链接查看:

Linux: Linux学习 - Gitee.com

相关文章:

  • C语言_函数hook_LD_PRELOAD原理和示例
  • opencv处理图像(二)
  • 进阶二:基于HC-SR04和LCD1602的超声波测距
  • 【ts】for in对象时,ts如何正确获取对应的属性值
  • sched_fair 调度:负载权重、虚拟运行时间与最小虚拟时间
  • Js 判断浏览器cookie 是否启用
  • 2025盘古石初赛WP
  • linux 开发小技巧之git增加指令别名
  • 路由策略和策略路由的区别以及配置案例
  • 用Python绘制动态彩色ASCII爱心:技术深度与创意结合
  • FHE与后量子密码学
  • 解决使用宝塔Linux部署前后端分离项目遇到的问题
  • Nakama:让游戏与应用更具互动性和即时性
  • 相机Camera日志分析之八:高通Camx HAL架构opencamera三级日志详解及关键字
  • spring中的@Inject注解详情
  • linux perf top分析系统性能
  • 深入解析JavaScript变量作用域:var、let、const全攻略
  • [架构之美]从零开始整合Spring Boot与Maven(十五)
  • upload-labs靶场通关详解:第四关
  • 【typenum】 0 配置文件(Cargo.toml)
  • 巴基斯坦全面恢复领空开放
  • 印称一名高级官员在巴基斯坦发动的袭击中死亡
  • 本周看啥|喜欢二次元的观众,去电影院吧
  • 中方是否认同俄方关于新纳粹主义观点?外交部:联大曾多次通过相关决议
  • 总奖池超百万!第五届七猫现实题材征文大赛颁奖在即
  • 乡村快递取件“跑腿费”屡禁不止?云南元江县公布举报电话