1 TCP协议报头
源端口号、目的端口号:标识数据从哪里来,要发送到哪里去
4位首部长度:TCP报头长度,用于分离报头信息和有效载荷。
4个比特位最大值为15,以四字节为单位(15*4 = 60),TCP报头长度20-60字节
6位保留位:现在还没有具体用处,方便以后去扩展
16位窗口大小:进行流量控制
16位紧急指针:标识那部分数据是紧急数据。优先处理数据的偏移量(优先处理的数据也称为“带外数据”)
选项:用于对报头信息的扩展
2 序号和确认序号
序号m:是指给发送数据的编号m
确认序号n:是指编号为n以前的数据都收到了,下次发送数据可以从n开始发送
序号和确认序号的作用:
(1)提供TCP的确认机制和重传机制;(确认机制:确认序号确认数据收到,重传机制:若发送数据的序号为1000但确认序号为1000,则说明编号为1000的数据并未收到,需要重传)
(2)保证数据的按序到达;(序号表明数据发送的顺序,确认序号依照序号的顺序)
(3)可以批量化的处理一堆报文;(发送数据的序号分别为:1000、1001、1002、1003,确认序号只有:1004,则表明1004以前的数据全部收到)
(4)数据去重。(若发送端A重复发送数据1000,接收端B发现自己已经确认过1000了,便不再确认,直至有未确认的数据到达时才确认)
为什么要有序号和确认序号一对序号,只有一个序号不可以吗?
不可以,因为TCP是全双工的,主机A、B间需要互发数据,只有一个序号无法做到。
3 6位标志位
URG:紧急指针标记位。置1,表明报头中的“紧急指针”有效,该报文需要优先处理
ACK:表明确认序号是否有效。一般来说,除建立连接请求的报文外,其余报文都需设置该位
PSH:提示接收端需要马上把缓冲区的内容读走。当接收端缓冲区快满的时候,发送端提示接收端将缓冲区中数据读走,否则发送端可能无法发送给数据
RST:对方请求重新建立连接。一般为“三次握手”过程中未建立好连接
SYN:请求建立连接。
FIN:请求断开连接,通知对方本端要关闭了。
4 TCP协议特点
(1)面向连接:有建立连接的“三次握手”和“四次挥手”
(2)可靠:有确认机制和重传机制等保证TCP的可靠性
(3)面向字节流:读取报文不用一个报文一个报文长度的读取,可以任意读取
(4)全双工:发送端和接收端双方可以同时通信
【注】:TCP既有发送缓冲区也有接收缓冲区
5 TCP的粘包问题
粘包问题是指由于TCP是面向字节流的,发送和接收时不需要一个报文一个报文的进行,所以在应用层拿到数据时就会是一串连续的字符串,不知道哪里是一个数据包的开始,哪里是该数据包的结尾。
如何解决粘包问题?
解决粘包问题的关键在于明确两个包之间的边界!!!
(1)采用定长报文的方式,每次都按照固定的长度读取,如:UDP协议,在报头中有报头的长度
(2)采用自描述字段,如:HTTP协议报头中的Content_Length字段,在报头位置告诉报文的长度
(3)采用明显的分割符,如:HTTP协议报头与报文中的空行
6 编写TCP——socket套接字
TCP服务器,实现功能:接收客户端数据,并将数据回显给客户端
单进程版本TCP服务器:有多个连接时会被阻塞
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SIZE 128
int startup(int port,char* ip)
{
//创建套接字
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
printf("socket error!\n");
return 2;
}
//填充本地套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
//绑定端口号
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
printf("bind error!\n");
return 3;
}
//设置为监听状态
if(listen(sock,5) < 0)
{
printf("listen error!\n");
return 4;
}
return sock;
}
void service(int sock,int port,char* ip)
{
char buf[SIZE];
while(1)
{
buf[0] = 0;
ssize_t s = read(sock,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("[%s:%d] say: %s\n",ip,port,buf);
write(sock,buf,strlen(buf));
}
else if(s == 0)//read返回值为0,表示读到文件结束,client关闭
{
printf("client [%s:%d] quit!\n",ip,port);
break;
}
else
{
printf("read error!\n");
break;
}
}
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s [ip][port]\n",argv[0]);
return 1;
}
int listen_sock = startup(atoi(argv[2]),argv[1]);
struct sockaddr_in peer;//远端
while(1)
{
socklen_t len = sizeof(peer);
//获得新的链接accept
int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
if(new_sock < 0)
{
printf("accept error!\n");
continue;
}
//链接成功,显示链接的ip地址和端口号
printf("get new connect,[%s:%d]\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
service(new_sock,ntohs(peer.sin_port),inet_ntoa(peer.sin_addr));
close(new_sock);
}
return 0;
}
TCP客户端,实现功能:发送数据,显示服务器回显数据,实现简单的客户端——服务器通信
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SIZE 128
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s [ip][port]\n",argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
printf("client sock error!\n");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
//向服务器发起链接connect
socklen_t len = sizeof(server);
int ret = connect(sock,(struct sockaddr*)&server,len);
if(ret < 0)
{
printf("connect error!\n");
return 3;
}
printf("connect success!\n");
char buf[SIZE];
while(1)
{
printf("please enter#");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s-1] = 0;
if(strcmp("quit",buf) == 0)
{
printf("client quit!\n");
break;
}
write(sock,buf,strlen(buf));
s = read(sock,buf,sizeof(buf)-1);
buf[s] = 0;
printf("server echo# %s\n",buf);
}
}
close(sock);
return 0;
}
结果显示:
服务器不可能只连接一个客户端,所以单进程版本的TCP服务器在实际使用时无法使用,因此有多进程和多线程版本的TCP服务器,实现同时处理多个链接请求。
多进程TCP服务器:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SIZE 128
int startup(int port,char* ip)
{
//创建套接字
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
printf("socket error!\n");
return 2;
}
//填充本地套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
//绑定端口号
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
printf("bind error!\n");
return 3;
}
//设置为监听状态
if(listen(sock,5) < 0)
{
printf("listen error!\n");
return 4;
}
return sock;
}
void service(int sock,int port,char* ip)
{
char buf[SIZE];
while(1)
{
buf[0] = 0;
ssize_t s = read(sock,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("[%s:%d] say: %s\n",ip,port,buf);
write(sock,buf,strlen(buf));
}
else if(s == 0)
{
printf("client [%s:%d] quit!\n",ip,port);
break;
}
else
{
printf("read error!\n");
break;
}
}
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s [ip][port]\n",argv[0]);
return 1;
}
int listen_sock = startup(atoi(argv[2]),argv[1]);
struct sockaddr_in peer;//远端
while(1)
{
socklen_t len = sizeof(peer);
//获得新的链接accept
int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
if(new_sock < 0)
{
//printf("accept error!\n");
perror("accept");
continue;
}
//链接成功,显示链接的ip地址和端口号
printf("get new connect,[%s:%d]\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
//多进程版本:父进程接受新链接,子进程提供服务
//为防止子进程变为僵尸进程,创建孙子进程,让孙子进程提供服务,
//这样即使子进程变为僵尸态,孙子进程会被1号进程领养,有效防止回收问题,
//同时此时父进程和孙子进程没有等待的关系,可以有效的使两个进程并行
pid_t id = fork();
if(id == 0)//child
{
close(listen_sock);//子进程不需要listen_sock
if(fork() > 0)
exit(0);
service(new_sock,ntohs(peer.sin_port),inet_ntoa(peer.sin_addr));
close(new_sock);
exit(0);
}
else if(id > 0)//father
{
close(new_sock);//父进程不需要new_sock,节省文件描述符资源
waitpid(id,NULL,0);
}
else
{
printf("fork error!\n");
continue;
}
}
return 0;
}
多线程TCP服务器:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SIZE 128
typedef struct Arg
{
int sock;
int port;
char addr[24];
}Arg;
int startup(int port,char* ip)
{
//创建套接字
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
printf("socket error!\n");
return 2;
}
//填充本地套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
//绑定端口号
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
printf("bind error!\n");
return 3;
}
//设置为监听状态
if(listen(sock,5) < 0)
{
printf("listen error!\n");
return 4;
}
return sock;
}
void service(int sock,int port,char* ip)
{
char buf[SIZE];
while(1)
{
buf[0] = 0;
ssize_t s = read(sock,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("[%s:%d] say: %s\n",ip,port,buf);
write(sock,buf,strlen(buf));
}
else if(s == 0)
{
printf("client [%s:%d] quit!\n",ip,port);
break;
}
else
{
printf("read error!\n");
break;
}
}
}
void* pthread_run(void* ptr)
{
Arg* arg = (Arg*)ptr;
service(arg->sock,arg->port,arg->addr);
close(arg->sock);
free(arg);
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s [ip][port]\n",argv[0]);
return 1;
}
int listen_sock = startup(atoi(argv[2]),argv[1]);
struct sockaddr_in peer;//远端
//char* ipbuf = &peer.sin_addr;
while(1)
{
socklen_t len = sizeof(peer);
//获得新的链接accept
int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
if(new_sock < 0)
{
printf("accept error!\n");
continue;
}
//链接成功,显示链接的ip地址和端口号
printf("get new connect,[%s:%d]\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
pthread_t tid;
Arg* arg =(Arg*)malloc(sizeof(Arg));
arg->port = ntohs(peer.sin_port);
arg->sock = new_sock;
strcpy(arg->addr,inet_ntoa(peer.sin_addr));
pthread_create(&tid,NULL,pthread_run,(void*)arg);
pthread_detach(tid);
//service(new_sock,ntohs(peer.sin_port),inet_ntoa(peer.sin_addr));
}
return 0;
}
多进程和多线程版本TCP服务器比较:
(1)多进程和多线程服务器都同时可以处理多个客户端请求;
(2)两者都编写简单;
(3)多进程服务器在连接到达之后才创建进程,需要时间,性能受损;
多线程服务器也存在同样的问题,但创建线程需要的时间比进程短,所以比多进程模式稍微好一点;
(4)随着进程的增多,CPU调度的压力增大,客户端等待的时间增加,影响性能;
多线程也存在相同的问题,但比进程稍好些;
(5)多进程服务器每个进程占用资源,而系统的资源又上限,所以只能服务有限个进程;
多线程同样,只不过线程占用的资源比进程少;
(6)多进程服务器稳定性强,一个进程异常不会影响其他进程;
多线程服务器稳定性差,有可能因为一个线程出现异常而导致整个服务器崩掉。