本文共 3873 字,大约阅读时间需要 12 分钟。
过程描述:
类比打电话。 对于服务器,首先创建套接字,socket;之后绑定一个端口,bind;进入监听状态,listen;等到对方打电话,accept;之后一直阻塞等到客户端连接过来。 对于客户端,首先要创建套接字,之后尝试打电话,即connect,一旦拨打通了,即连接上了,开始TCP的三次握手。(详细见下面分析)建立连接后,客户端,和服务器听过write和read进行数据请求,和数据应答。都是write发,read收。
客户端想关闭,close,然后服务器,read后,也close。
socket:创建套接字,用于通信。
bind
注意这里的输入参数,通用地址结构。
listen:调用socket和bind函数之后,调用accept之前调用。listen:是将套接字从主动套接字转化为被动套接字
主动套接字:发起连接 connect 被动套接字:接收连接 accept对于给定的监听套接字,内核要维护两个队列:
1。 已有客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程。2。 已完成连接的队列。
accept
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())。
参考:
(未完待续,有点乱)