TCP客户/服务器模型

时间:2021-02-02 00:17:39

CS模型:Client /Service模型

TCP客户/服务器模型

过程描述:
类比打电话。
对于服务器,首先创建套接字,socket;之后绑定一个端口,bind;进入监听状态,listen;等到对方打电话,accept;之后一直阻塞等到客户端连接过来。
对于客户端,首先要创建套接字,之后尝试打电话,即connect,一旦拨打通了,即连接上了,开始TCP的三次握手。(详细见下面分析)

建立连接后,客户端,和服务器听过write和read进行数据请求,和数据应答。都是write发,read收。

客户端想关闭,close,然后服务器,read后,也close。

TCP三次握手

回射客户/服务器

TCP客户/服务器模型

重要函数

socket:创建套接字,用于通信。
TCP客户/服务器模型

bind
TCP客户/服务器模型

注意这里的输入参数,通用地址结构。
listen:调用socket和bind函数之后,调用accept之前调用。
TCP客户/服务器模型

listen:是将套接字从主动套接字转化为被动套接字
主动套接字:发起连接 connect
被动套接字:接收连接 accept

对于给定的监听套接字,内核要维护两个队列:
1。 已有客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程。

2。 已完成连接的队列。

accept
TCP客户/服务器模型

connect

服务器端:

//if ((conn=accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)//服务端:等待客户端接入.conn对应的是客户端发出来的套接字
//{
// ERR_EXIT("accept");
//}

char recvbuf[1024];
while (1)
{
conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen); //如果没有错误产生,则accept()返回一个描述所接受包的SOCKET类型的值。否则的话,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetLastError()来获得特定的错误代码。
cout << conn << endl;
memset(recvbuf, 0, sizeof(recvbuf));
char sendBuf[50];
sprintf(sendBuf, "Welcome %s to here!", inet_ntoa(peeraddr.sin_addr));
cout << sendBuf << endl;
send(conn, sendBuf, strlen(sendBuf) + 1, 0);
int ret=recv(conn, recvbuf, sizeof(recvbuf), 0);
cout << recvbuf << endl;
//send(conn, recvbuf, strlen(recvbuf), MSG_PEEK);
}

closesocket(conn);

客户端:

connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
send(sockClient, "hello", strlen("hello") + 1, 0);
char recvBuf[50];
recv(sockClient, recvBuf, 50, 0);
printf("%s\n", recvBuf);

此时服务器端的while(1)只执行一次,为啥?是因为accept()要收到数据包才能有返回值,而客户端只发送一次,所以while(1)里的accept在收到数据包一次后,就继续接收,进入等待。但是如果把accept放到外面,里面的while(1)就一直执行了,前提是客户端必须发送一次数据包,让accept函数执行完,才执行while(1)。这个很容易理解。

我觉得好的做法就是把accept放到while(1)里。这样客户端每次有新数据时,都会打印出来。但是经过测试发现我前面的理解有点不对。

while(1)里的accept是,只要我客户端关闭连接,里面就不执行了,因为accept一直在等待。但是如果我客户端修改成下面:

    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
cout << send(client_sock, sendbuf, strlen(sendbuf), 0) << endl;//发送,并输出返回字符串长度
recv(client_sock, recvbuf, sizeof(recvbuf), 0);
cout << recvbuf << endl;
}

一直从stdin中输入,服务器只能接收第一次的数据并打印出来,后面的再次输入已经无法接收了。 原因我还没搞清楚。应该和accept的机制有关。

好的做法还是把accept放到外面。

if ((conn=accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)//服务端:等待客户端接入.conn对应的是客户端发出来的套接字
{
ERR_EXIT("accept");
}

char recvbuf[1024];
while (1)
{
//conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen); //如果没有错误产生,则accept()返回一个描述所接受包的SOCKET类型的值。否则的话,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetLastError()来获得特定的错误代码。
//三个参数:服务器的这边创建的套接字,接收到的数据包存储的套接字(类似于客户端的),长度peerlen。 conn:描述所接受包的SOCKET类型的值
cout << conn << endl;
memset(recvbuf, 0, sizeof(recvbuf));
char sendBuf[50];
sprintf(sendBuf, "Welcome %s to here!", inet_ntoa(peeraddr.sin_addr));//打印的是客户端的地址,并把给地址通过sendBuf给客户端发送过去
cout << sendBuf << endl;
send(conn, sendBuf, strlen(sendBuf) + 1, 0);//给客户端发
int ret=recv(conn, recvbuf, sizeof(recvbuf), 0);
cout << recvbuf << endl;
//send(conn, recvbuf, strlen(recvbuf), MSG_PEEK);

}

另外,客户端程序进行修改:

    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
cout << send(client_sock, sendbuf, strlen(sendbuf), 0) << endl;//发送,并输出返回字符串长度
recv(client_sock, recvbuf, sizeof(recvbuf), 0);
cout << recvbuf << endl;
}

上述实现的是从客户端输入,服务器端显示出来。

服务器端编程的步骤

sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW);基于TCP的socket编程是采用的流式套接字。

服务器端编程的步骤:

1:加载套接字库,创建套接字(WSAStartup()/socket());

2:绑定套接字到一个IP地址和一个端口上(bind());

3:将套接字设置为监听模式等待连接请求(listen());

4:请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());

5:用返回的套接字和客户端进行通信(send()/recv());

6:返回,等待另一连接请求;

7:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。

客户端编程的步骤:

1:加载套接字库,创建套接字(WSAStartup()/socket());

2:向服务器发出连接请求(connect());

3:和服务器端进行通信(send()/recv());

4:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。

参考:http://www.cnblogs.com/Sniper-quay/archive/2011/06/22/2086636.html

(未完待续,有点乱)