一、初识TCP协议
TCP协议是面向链接,可靠的,基于字节流传输层协议 。使用严格的应答机制来保证可靠性。
1、建立连接时进行三次握手
2、断开连接时进行四次挥手

TCP服务端的创建流程:
1、建立 socket,SOCK_STREAM
2:绑定ip地址与端口号
3:建立监听队列,让套接字进入到被动监听状态:int listen(int sockfd, int backlog);
4 : 接受连接,产生新的套接字:

5 : 数据读写
1)发送:
2)接收 :

二、并发服务器
问题:多个客户端需要处理,请求可能同时到来
解决办法:采用并发服务器。思想:多任务处理机制(多线程或者多进程), 分别为每个客户端创建一个任务来处理,极大的 提高了服务器的并发处理能力。
1、IO多路复用 (IO multiplexing):用单个进程对多个IO进行操作
三、select函数
允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
1、select 函数声明如下
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
原理:
1)将需要进行io的文件描述符添加到文件描述符集合
2)将这个文件描述符集合拷贝到内核中
3)内核需要遍历文件描述符集合,循环检测对应的文件描述符是否可以进行io操作(可读 or 可写)
2、参数说明:
1)nfds:int类型,指集合中所有文件描述符的范围。即所有文件描述符的最大值加1(maxfd + 1)。在linux系统中,select的默认最大值为1024。设置这个值的目的是为了不用每次都去轮询这1024个fd,假设我们只需要几个套接字,我们就可以用最大的那个套接字的值加上1作为这个参数的值,当我们在等待是否有套接字准备就绪时,只需要监测maxfd+1个套接字就可以了,这样可以减少轮询时间以及系统的开销。
2)readfds:首先需要明白,fd_set是什么数据类型,有一点像int,又有点像struct,其实,fd_set声明的是一个集合,也就是说,readfs是一个容器,里面可以容纳多个文件描述符,把需要监视的描述符放入这个集合中,当有文件描述符可读时,select就会返回一个大于0的值,表示有文件可读。
3)writefds:和readfs类似,表示有一个可写的文件描述符集合,当有文件可写时,select就会返回一个大于0的值,表示有文件可写。
4)fd_set *exceptfds:其他文件描述符集合
5)timeout:可以用select来做超时处理。可以选择阻塞,可以选择非阻塞,还可以选择定时返回。当将timeout置为NULL时,表明此时select是阻塞的;当将tineout设置为timeout->tv_sec = 0,timeout->tv_usec = 0时,表明这个函数为非阻塞;当将timeout设置为非0的时间,表明select有超时时间,当这个时间走完,select函数就会返回。
struct timeval{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
}
四、select函数编写TCP服务器
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#define BACKLOG 10
/*struct timeval {
long tv_sec;
long tv_usec;
};*/ //时间结构体
int main(int argc, char **argv)
{
int i,ret,nmfds;
int sfd,cfd,maxfd;
fd_set rdfds,tmpfds;//原始表单,备份表单
struct timeval tim1,tim2;//原始时间,备份时间
ssize_t recv_bytes, send_bytes;
char buf[128] = {0};
struct sockaddr_in src,cli;
socklen_t len = sizeof(src);
socklen_t addrlen = sizeof(cli);
sfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字,第二个参数为流式套接字类型
if(sfd == -1)
{
perror("socket fail");
return -1;
}
src.sin_family = AF_INET;//服务器地址结构体填充,地址族,IP PORT
src.sin_port = htons(atoi(argv[2]));
src.sin_addr.s_addr = inet_addr(argv[1]);
ret = bind(sfd, (const struct sockaddr *)&src, len );//绑定套接字,服务器ip和port口号
if(ret == -1)
{
perror("bind");
return -1;
}
ret = listen(sfd, BACKLOG);//将套接字设定为被动监听状态,监听客户端的连接请求,10为未决队列长度
if(ret == -1)
{
perror("listen");
return -1;
}
FD_ZERO(&rdfds);//将2个表单清0
FD_ZERO(&tmpfds);
FD_SET(sfd, &rdfds);//将监听套接字加入表单
tim1.tv_sec = 3;//超时时间设置为3s
tim1.tv_usec = 0;
maxfd = sfd;//最大的文件描述符设置为sfd
while(1)
{
tmpfds = rdfds;//将原始表单赋值给备份表单
tim2 = tim1;//将原始时间赋值给备份时间
nmfds = select(maxfd + 1, &tmpfds, NULL, NULL, &tim2);
if(nmfds == -1)
{
perror("select fail");
return -1;
}else if(nmfds == 0)//超时处理
{
printf("timeout\n");
}else//有文件描述符就绪
{
for(i = 0; i <= maxfd; i++)//遍历所有关注的文件描述符
{
if(FD_ISSET(i, &tmpfds))//判断文件描述符是否就绪
{
if(i == sfd)//监听套接字就绪
{
cfd = accept(sfd, (struct sockaddr *)&cli,&addrlen);//接收客户端连接请求,产生连接套接字,用于和客户端通信,通过cli保存客户端ip地址和port口号
if(cfd == -1)
{
perror("accept");
return -1;
}
printf("client IP: %s PORT: %d\n", inet_ntoa(cli.sin_addr),ntohs(cli.sin_port));//打印客户端IP 和 PORT ,需要将网络字节序转换为主机字节序
FD_SET(cfd, &rdfds);//将新产生的连接套接字加入到原始表单
maxfd = (cfd > maxfd)? cfd:maxfd;//更新最大的文件描述符
}else if(i > sfd)//连接套接字就绪
{
memset(buf,0,sizeof(buf));
recv_bytes = recv(i, buf, sizeof(buf),0);//接收客户端的信息储存到buf中
if(recv_bytes == -1)
{
perror("recv");
return -1;
}else if(recv_bytes == 0)//客户端退出返回0
{
printf("client shutdown\n");
close(i);//关闭相应的文件描述符
FD_CLR(i, &rdfds);//将文件描述符清除出原始表单
}else
{
printf("buf: %s\n",buf);
}
}
}
}
}
}
return 0;
}
五、TCP客户端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int sockfd;
int ret;
ssize_t send_bytes,recv_bytes;
char buf[128] = {0};
char buf1[128] = {0};
struct sockaddr_in dest;
socklen_t len = sizeof(dest);
if(argc < 3)
{
fprintf(stderr, "usage: %s [IP] [PORT]\n",argv[0]);
return -1;
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if(sockfd == -1)
{
perror("");
return -1;
}
dest.sin_family = AF_INET;//填充服务器的ip地址和port口号
dest.sin_port = htons(atoi(argv[2]));
dest.sin_addr.s_addr = inet_addr(argv[1]);
ret = connect(sockfd, (const struct sockaddr *)&dest, len);//与服务器建立连接
if(ret == -1)
{
perror("connect fail");
return -1;
}
while(1)
{
memset(buf, 0 ,sizeof(buf));
memset(buf, 0 ,sizeof(buf1));
fgets(buf, sizeof(buf),stdin);
send_bytes = send(sockfd, buf, strlen(buf), 0);//向服务器发送数据
if(send_bytes == -1)
{
perror("send ");
return -1;
}
recv_bytes = recv(sockfd, buf1, sizeof(buf1),0);//接收服务器发来的数据,没有收数据会阻塞在这里
if(recv_bytes == -1)
{
perror("recv ");
return -1;
}
}
return 0;
}