TCP提供的是面向连接、可靠的、字节流服务
客户端与服务端分别在两个不同的主机上,TCP连接流程固定
首先第一步是创建套接字socket()
(客户端创建套接字一般不需要指定端口号),相当于区分不同的连接,然后绑定bind()
来指定ip与端口,然后创建监听队列listen()
,等待接收连接accept()
会阻塞在这一步等待链接,这个时候客户端执行connect()
建立链接然后会进行三次握手进行连接,随后客户端与服务端就可以发送接收数据了,close()
关闭套接字
主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。 在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。
不同主机对字节的存储顺序可能不一样,存在小段字节序和大段字节序
Linux系统提供了如下4个函数来完成主机字节序和网络字节序之间的转换:
较常用的:uint16_t htons(uint16_t hostshort)
主机序列转网络序列,十六位较多
socket网络编程接口中表示socket地址的是结构体sockaddr(对链接到唯一表示),其定义如下:
这是通用套接字结构
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分
别用于 IPV4 和 IPV6:
通常,人们习惯用点分十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换:
将字符串,转为32位整型
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr,socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
int close(int sockfd);
ssize_t recv(int sockfd, void *buff, size_t len, int flags);
ssize_t send(int sockfd, const void *buff, size_t len, int flags);
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
//创建套接字
//参数:
//AF_INET 指定协议族为ipv4
//SOCK_STREAM 服务类型 使用流式套接字
//一般为0
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
//创建套接字地址结构
struct sockaddr_in saddr,caddr;//caddr存放客户端的ip与端口
memset(&saddr,0,sizeof(saddr));//清空saddr
saddr.sin_family = AF_INET;//协议族或者叫地址族
saddr.sin_port = htons(6000);//端口 转换主机序列到网络序列
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip 字符串转换为整形,127.0.0.1是用于本地测试的ip地址
//绑定套接字
//参数:
//套接字描述符
//地址结构,需要传入通用套接字结构所有进行强转
//地址结构大小
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
//创建监听队列
//参数:
//套接字描述符
//监听队列长度
listen(sockfd,5);
while(1)
{
//循环 接受客户端的连接
//参数:
//套接字描述符
//存放客户端的地址结构
//地址结构大小
int len = sizeof(saddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c < 0)
{
continue;
}
printf("accept c = %d\n",c);
char buff[128] = {
0};
recv(c,buff,127,0);
//从c 也就是客户端接受数据,存放在buff,大小为127,防止将其占满,标志位给0
//也可以通过read操作,因为c也是文件描述符
//实际上是与c进行沟通,而sockfd只作用于循环前
printf("buff = %s\n",buff);
send(c,"ok",2,0);
//发送给c客户端,发送OK,大小为2,标志位0
//也可以用write(),与上同理
close(c);//关闭c
}
}
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
//socket() connect() send() recv() close()
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr;//客户端(caddr)自己的端口ip由系统分配
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
printf("input:\n");
char buff[128] = {
0};
fgets(buff,127,stdin);
send(sockfd,buff,strlen(buff),0);//write()也可以做到
memset(buff,0,128);//清空为了接受 来自服务渠道数据
recv(sockfd,buff,127,0);//read()也可以做到
printf("read:%s\n",buff);
close(sockfd);
}
我们将服务器与客户端代码都运行起来查看
服务端在这里是死循环一直执行的,只能手动暂停
当我们运行了服务端之后,6000号端口就会被我们占用,我们输入 netstat -natp
指令查看
我们看到 “127.0.0.1:6000” 的信息,当我们再次执行服务端代码,就会失败,因为端口已经被占用了
只有端口被释放出来,才能再次执行
这里的消息队列长度,可以理解为有座位,座位的数目就是我们的长度,当我们listen后会创建两个队列,一个是未完成三次握手的,一个是完成三次握手的;当我们客户端发起链接后(connect)就会将链接放在上面未完成三次握手的队列中,等完成三次握手就放入下面的队列中,而accept就相当于从完成三次握手的队列中拿到链接进行处理得到一个新的c,就可以通过c进行交互,完成三次握手的队列长度就是我们设置的5,这里之所以设置长度,就是为了防止等待造成的问题,并且这里的5并不代表服务端只能处理5个客户端
listen并不会阻塞,而accept会阻塞,例如我们在此处代码加一行打印,我们编译运行服务器后会发现,服务器代码会阻塞在这一步
等待客户端的连接,这时候运行客户段代码
就会继续运行,阻塞在recv处等待接收数据,而客户端阻塞在fgets处,客户端输入数据后服务端继续运行
这时候客户端运行结束,服务器继续阻塞等待新的客户段连接,我们再次运行客户端代码,服务端就会接着下一步
服务段的ip与端口,在写客户端代码的时候需要自己添加进去,而客户端的ip与端口则是由系统自动生成的,其他更多的在代码注释中介绍
当我们通过TCP发送数据,并不是直接发送给对方,而是先将数据存放在发送缓冲区中,形成字节流;然后按照底层协议将数据重新打包再发送给对方,对方得到打包的数据,再将其放在接收TCP缓冲区,再接收到的数据就是一整个结合
这样看就会出现我们说的粘包问题(多次发送的数据会被对方一次收到),粘包问题有时候会有影响有时候没有影响,比如我们传输一个文件,至于数据发送几次接受几次并不影响文件传输的目的,而假如我们去发送长方体的长宽高,需要接收方返回体积,这时候我们send()发送了三次,发生粘包问题,就会只收到一次就不能区分长宽高
我们可以通过一个方法来解决这类问题,再send()之后,加一个recv()来接收对方接收成功发来的讯息,这样就可以将不同的数据进行错开;或者可以通过将发送数据按某种格式进行分割(【3】【4】【5】,例如这样通过中括号进行分割)
我们在执行send()
的时候只能说明数据放在了发送缓冲区中,但是是否发送给了对方的接收缓冲区我们并不知道
我们将原先服务端代码中的循环部分进行修改如下:
while(1)
{
int len = sizeof(saddr);
printf("accept wait ......\n");
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c < 0)
{
continue;
}
printf("accept c = %d\n",c);
while(1)
{
char buff[128] = {
0};
int n = recv(c,buff,127,0);
if(n<=0)
{
break;
}
printf("buff = %s\n",buff);
send(c,"ok",2,0);
}
close(c);
}
原本只进行一次接收并打印,随后就会关闭链接,现在我们将其循环起来,只要对方不关闭链接(recv返回值为0)就能不断接收数据
我们将原本的客户端代码也进行修改
while(1)
{
printf("input:\n");
char buff[128] = {
0};
fgets(buff,127,stdin);
if(strncmp(buff,"end",3) == 0)
{
break;
}
send(sockfd,buff,strlen(buff),0);//write()也可以做到
memset(buff,0,128);//清空为了接受 来自服务渠道数据
recv(sockfd,buff,127,0);//read()也可以做到
printf("read:%s\n",buff);
}
close(sockfd);
将原本只发送并接受一次数据就关闭链接,修改为只要不从键盘获取end
,就不断发送获取数据
我们将代码编译运行起来
我们再次修改服务端的代码,我们将此处本来只能获得127字节大小改为1,每次接收1字节并打印
我们再次运行代码查看
有些主机可能会出现,直接受到一个ok的返回
我们使用netstat -natp
来查看
可以看到客户端与服务端的端口与ip,再去观察我们的接收缓冲区,假如向下下图那样缓冲区并不为空,说明还有数据没有被recv接收(Recv-Q 接收缓冲区 Send-Q 发送缓冲区)