TCP:Transmission Control Protocol,传输控制协议。
实现基于TCP的服务器端/客户端
TCP服务端的默认函数调用顺序
socket()-bind()-listen()-accept()-r/w-close()
进入连接请求状态:
当已经调用bind函数给套接字分配了地址之后,接下来就要通过调用listen函数进入等待连接请求状态。只有调用了listen函数,客户端才能进入可发出连接请求的状态。换言之,这时客户端才能调用connect函数。
#include <sys/socket.h>
int listen(int sock, int backlog);
//成功返回0,失败返回-1
sock:希望进入等待连接请求状态的套接字文件描述符,然后成为服务器端套接字(监听套接字)。
backlog:连接请求队列的长度。
调用listen函数可以生成监听套接字,用来接受客户端连接请求(这本身也是一种数据)。
受理客户端连接请求:
调用listen函数后,若有新的连接请求,则会按序受理。受理请求代表着进入可接受数据的状态。
那么必然就需要一个新的套接字(监听套接字还需要监听着)。accept函数将自动创建套接字,并连接到发起请求的客户端。
#include <sys/socket>
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
//成功是返回创建的套接字文件描述符,失败时返回-1
sock:服务器套接字的文件描述符。
addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。
addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用后,该变量即被填入客户端地址长度。
TCP客户端的默认函数调用顺序
socket()-connect()-r/w-close()
与服务器端相比,区别就在于connect,它是创建客户端套接字后向服务器端发起的连接请求。服务器端调用listen函数后创建连接请求等待队列,之后客户端即可请求连接。
#include <sys/socket.h>
int connect(int sock, struct sockaddr *servaddr, socklen_t addrlen);
//成功返回0,否则-1
sock:客户端套接字文件描述符
servaddr:保存目标服务器端地址信息的变量地址值。
addrlen:servaddr的地址变量的长度
客户端调用connect函数后,发生以下情况之一才会返回。
- 服务器端接受连接请求
- 发生断网等异常情况而中断连接请求
需要注意,所谓的“接受连接”并不意味着服务器端调用accept函数,其实是服务器端把连接请求信息记录到等待队列。
因此connect函数返回后并不立即进行数据交换。
注意:客户端套接字的ip地址和端口在调用connect函数时自动分配,无需调用标记的bind函数进行分配。
实现迭代服务器端/客户端
简单来说就是反复调用accept函数,使服务器可以反复接受客户端连接请求。
服务器端主要代码:
for(int i=0;i<5;i++){//为处理5个客户端连接而添加的循环语句。共调用5次函数。
int clnt_sock = accept(serv_sock,(struct sockaddr *)&clnt_adr,&clint_adr_sz);
if(clnt_sock==-1) error("accept error");
else printf("Connected client %d\n",i+1);
ssize_t len=0;
//while遇EOF结束
while((len=read(clnt_sock, message, SIZE))!=0){
message[len]=0;
printf("%d:%s\n",++cnt,message);
write(clnt_sock, message, len);
}
close(clnt_sock);//针对套接字调用close函数,向连接的相应套接字发送EOF。
//换言之,客户端套接字若调用close函数,则第50行的循环条件变成false,所以往下执行。
}
close(serv_sock);//最终关闭监听套接字
客户端主要代码:
while(1){
printf("Input Q to exit \n");
scanf("%s",message);
if(strcmp(message,"Q\n")==0) break;
write(sock, message, strlen(message));
ssize_t len=read(sock, message,SIZE-1);
message[len]=0;
printf("Message from server: %s\n",message);
}
close(sock);//调用close向相应套接字发送EOF(EOF意味着中断连接)
回声客户端存在的问题:
write(sock, message, strlen(message));
ssize_t len=read(sock, message,SIZE-1);
message[len]=0;
printf("Message from server: %s\n",message);
以上代码有个错误假设:
每次调用r/w函数使都会以字符串为单位执行实际的I/O操作。
但是因为TCP不存在数据边界。因此,多次调用write函数传递的字符串有可能一次性传递到服务器端。此时客户端有可能从服务器端收到多个字符串。这不是我们希望看到的结果。也需要考虑服务器端的如下情况:字符串太长,需要分2个数据包发送。
数据太大,操作系统就有可能把数据分成多个数据包发送给客户端。在此过程中,客户端可能在尚未收到全部数据包时就调用read函数。