TCP Socket编程

发布于:2025-05-11 ⋅ 阅读:(20) ⋅ 点赞:(0)

最基本的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和port
            struct 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.accept
            while(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