最基本的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));
}
}
上面的代码是部分代码,具体的代码可以通过下面链接查看: