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