TCP服务端/客户端
TCP与UDP
TCP与UDP是传输层常见的网络协议。TCP/IP协议栈如图:
最底层为数据链路层,往上是IP层,再通过TCP或UDP到达应用层。
链路层负责点到点的传输,而IP层负责端到端的传输。但IP协议是不可靠的协议,数据丢失或者错误的情况无法解决。于是TCP就负责数据传输的可靠性。收到后确认,需要则重传。
应用层
我们所说的socket编程,其实都是发生在应用层的,大部分内容是设计和实现应用层的协议,上面的细节由socket自动处理。
TCP服务端的调用顺序为:
- socket()创建套接字
- bind()分配IP地址和端口号
- listen()等待连接请求
- accept()允许连接
- read()/write()数据交换
- close()断开连接
服务端:等待连接请求
在调用了listen()之后,客户端才可以调用connect()(想要建立连接先要接起电话)
#include <sys/socket.h>
int listen (int sock,int backlog);
第一个参数接收进入等待连接请求状态的套接字文件描述符(可以理解为编号),第二个参数是等待连接队列的长度。
可以理解为listen会生成一个门卫(服务端套接字),并设定等候室的大小,让请求的连接进入等候室。
服务端:允许连接
当有了新的请求时,需要新的套接字来进入这种状态,这时需要accept函数。
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
accept生成一个用于数据I/O的套接字,返回其文件描述符sockfd。第二个参数用于保存客户端地址信息。第三个参数用于保存客户端地址的长度。
简单来说就是从等待队列中取出一个连接请求,创建套接字然后完成连接。
客户端:请求连接
客户端请求连接时调用的是connect函数。
#include <sys/socket.h>
int connect(int sockfd,struct sockaddr *servaddr,socklen_t addrlen);
在服务端接收连接请求后,或者中断连接请求后,connect函数会返回。(接收连接不是accept,是进入到了服务端的请求队列)
服务端没有bind的过程。在调用connect函数时自动分配。
客户端与服务端的关系是:
回声服务器
回声的意思就是把服务端将客户端传来的字符数据再原封不动的传回去。
我们之前的模式都是处理完一个客户端请求就退出服务端。如果想要连续服务就需要用循环调用accept。
(如果想要同时服务多个客户端需要学习进程与线程)
我们来看回声服务器的核心代码:
int main(int argc,char* argv[])
{
int serv_sock,clnt_sock;
char message[BUF_SIZE];
int strlen,i;
struct sockaddr_in serv_adr,clnt_adr;
socklen_t clnt_adr_sz;
...
serv_sock=socket(PF_INET,SOCK_STREAM,0);
if(serv_sock==-1) error_handling("socket()error");
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock,(struct sockaddr*)&serv_adr),sizeof(serv_adr))==-1)
error_handling("bind()error");
if(listen(serv_sock,5)==-1)
error_handling("listen() error");
clnt_adr_sz=sizeof(clnt_adr);
for(int i=0;i<5;i++)
{
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
if(clnt_sock==-1)
error_handling("accept() error");
else
printf("Connected client %d \n",i+1);
while((str_len=read(clnt_sock,message,BUF_SIZE))!=0)
wirte(clnt_sock,message,str_len);
close(clnt_sock);
}
close(serv_sock);
return 0;
}
客户端我们只看socket建立之后的部分,也就是调用connect:
if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
error_handling("connect() error");
else
puts("Connected...")
while(1)
{
fputs("Input message(Q to quit):",stdout);
fgets(message,BUF_SIZE,stdin);
if(!strcmp(message,"q\n")||!strcmp(message,"Q\n"))
break;
write(sock,message,strlen(message));
str_len=read(sock,message,BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s",message);
}
close(sock);
return 0;
客户端是真正进行操作的地方。这里message存储输入的内容,如果是q则退出,不是就写入,再读出服务器内容。
两边相互协作。客户端写入的内容被服务端读出,再写回。
存在的问题
上述代码是有一些问题的。客户端中wirte想要写入message的内容,但对于TCP客户端来说,有可能出现多次写入一次性读出的情况。这样客户端就会一次性收到服务端传来的大量信息。
而且当字符串太长时,操作系统可能会把数据分成多个数据包发送,这时客户端可能没有收到全部数据包就调用read。
解决方案
1.提前确认接收数据的大小,然后在接受时循环调用read来读取
while(1)
{
fputs("Input message(Q to quit):",stdout);
fgets(message,BUF_SIZE,stdin);
if(!strcmp(message,"q\n")||!strcmp(message,"Q\n"))
break;
str_len=write(sock,message,strlen(message));
recv_len=0;
while(recv_len<str_len)
{
recv_cnt=read(sock,message,BUF_SIZE-1);
if(recv_cnt==-1)
error_handling...;
recv_len+=rev_cnt;
}
message[str_len]=0;
printf("Message from server: %s",message);
}
close(sock);
return 0;
可以对照一下与上面的区别,这样可以让write和read对等。
2.定义应用层协议
定义客户端和服务端传递数据的字节数,这样使得TCP传输变的有数据边界。
TCP原理补充
与对方套接字连接
再在TCP套接字与对方套接字连接时,会经历一个三次握手的过程。
套接字是全双工的。例如A到B
1.主机A发送数据包序号信息SYN
2.主机B返回确认以及B的数据包序号SYN+ACK
3.主机A返回确认信号ACK
与对方主机交换数据
交换数据时返回的确认号为:
ACK=SEQ+传递的字节数+1,这样可以看出是否丢失字节。
与对方套接字断开连接
断开连接的阶段也叫作四次挥手。
1.主机A发送数据包序号,希望断开
2.主机B返回确认号和数据包序号,开始准备
3.主机B发送确认号和数据包序号,可以断开
4.主机A返回确认号和数据包序号,确认断开
|