TCP头部格式说明
* 源端口和目的端口: 各占 2 字节.端口是传输层与应用层的服务接口.传输层的复用和分用功能都要通过端口才能实现
* 序号: Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
* 确认号: Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。
* 数据偏移/首部长度: 占 4 位,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远.“数据偏移”的单位是 32 位字(以 4 字节为计算单位)
* 保留: 占 6 位,保留为今后使用,但目前应置为 0
* 紧急URG: 当 URG=1 时,表明紧急指针字段有效.它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据)
* 确认ACK: 只有当 ACK=1 时确认号字段才有效.当 ACK=0 时,确认号无效。 不要将确认序号Ack与标志位中的ACK搞混了。
* PSH(PuSH): 接收 TCP 收到 PSH = 1 的报文段,就尽快地交付接收应用进程,而不再等到整个缓存都填满了后再向上交付
* RST (ReSeT): 当 RST=1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接
* 同步 SYN: 同步 SYN = 1 表示这是一个连接请求或连接接受报文
* 终止 FIN: 用来释放一个连接.FIN=1 表明此报文段的发送端的数据已发送完毕,并要求释放运输连接
* 检验和: 占 2 字节.检验和字段检验的范围包括首部和数据这两部分.在计算检验和时,要在 TCP 报文段的前面加上 12 字节的伪首部
* 紧急指针: 占 16 位,指出在本报文段中紧急数据共有多少个字节(紧急数据放在本报文段数据的最前面)
* 选项: 长度可变.TCP 最初只规定了一种选项,即最大报文段长度 MSS.MSS 告诉对方 TCP:“我的缓存所能接收的报文段的数据字段的最大长度是 MSS 个字节.” [MSS(Maximum Segment Size)是 TCP 报文段中的数据字段的最大长度.数据字段加上 TCP 首部才等于整个的 TCP 报文段]
* 填充: 这是为了使整个首部长度是 4 字节的整数倍
网络通信模型
网络编程流程
1.服务器端
①首先调用socket( )---成功后返回一个文件描述符fd
int socket(int family, int type, int protocol);
family指定协议族;type参数指定socket的类型:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW;protocol通常赋值"0";
②bind( )指定监听ip和目的端口号
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
sockfd是一个socket描述符,my_addr是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的针; addrlen常被设置为sizeof(struct sockaddr)。 最后,对于bind 函数要说明的一点是,你可以用下面的赋值实现自动获得本机IP地址和随机获取一个没有被占用的端口号:
my_addr.sin_port = 0; /* 系统随机选择一个未被使用的端口号 */
my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本机IP地址 */
通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。bind()函数在成功被调用时返回0;遇到错误时返回"-1"并将errno置为相应的错误号。另外要注意的是,当调用函数时,一般不要将端口号置为小于1024的值,因为1~1024是保留端口号,你可以使用大于1024中任何一个没有被占用的端口号。
③listen( )设定最大连接数,超过最大连接数会返回错误
int listen(int sockfd, int backlog);
sockfd是socket系统调用返回的服务器端socket描述符;backlog指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待accept()它们。backlog对队列中等待服务的请求的数目进行了限制,大多数系统缺省值为20。当listen遇到错误时返回-1,errno被置为相应的错误码。
④
int accept(int sockfd, struct sockaddr *addr, int *addrlen);
sockfd是被监听的服务器socket描述符,addr通常是一个指向sockaddr_in变量的指针,该变量用来存放提出连接请求的客户端地址;addrten通常为一个指向值为sizeof(struct sockaddr_in)的整型指针变量。错误发生时返回一个-1并且设置相应的errno值。accept()函数将返回一个新的socket描述符,来供这个新连接来使用,在新的socket描述符上进行数据send()和recv()操作。
⑤close()和shutdown()——结束数据传输
当所有的数据操作结束以后,你可以调用close()函数来释放该socket,从而停止在该socket上的任何数据操作:
int shutdown(int sockfd,int how);
sockfd的含义是显而易见的,而参数 how可以设为下列值:
·0-------不允许继续接收数据
·1-------不允许继续发送数据
·2-------不允许继续发送和接收数据,均为允许则调用close()
shutdown在操作成功时返回0,在出现错误时返回-1(并置相应errno)。
2.客户端
① connect()函数用来与远端服务器建立一个TCP连接其函数原型为:
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
sockfd是目的服务器的sockt描述符;serv_addr是服务器端的IP地址和端口号的地址。遇到错误时返回-1,并且errno中包含相应的错误码。进行客户端程序设计无须调用bind(),因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,内核会自动选择一个未被占用的端口供客户端来使用。
服务器端一旦调用accept(),只要客户端不调用connect(),会一直造成阻塞,知道客户端调用connect(),并且重新生成一个fd。
②bind()不调用时系统内核会自动分配一个可用端口,调用时可以设定一个指定端口。
服务器端程序
本程序代码
/*********************************************************************************
* Copyright: (C) 2018 Yujie
* All rights reserved.
*
* Filename: socket_server.c
* Description: server.c
*
* Version: 1.0.0(04/21/2018)
* Author: yanhuan <yanhuanmini@foxmail.com>
* ChangeLog: 1, Release initial version on "04/21/2018 09:21:24 PM"
*
********************************************************************************/
/*
1、创建套接字---socket
处理地址和端口
2、套接字绑定本地的地址和端口---band
3、套接字设定监听状态(等待客服端消息)---listen
4、接受消息,返回一个用连接的新套接字
5、新套接字通信---read,write
6、关闭套接字---close
*/
#include <sys/socket.h> //socket头文件
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define port 12000
#define buf_size 1024
int main (int argc, char **argv)
{
int listen_fd, accept_fd = 1; //服务器端需要两个套接字:一个开启连接,一个通信
struct sockaddr_in server_addr; //服务器端地址,是一个结构体,等下要填充ipV4或者ipV6的结构
char buf[buf_size];
listen_fd = socket(AF_INET,SOCK_STREAM,0); //创建套接字,ipV4地址,可靠的面向连接的字符流,最后0是默认协议
if(listen_fd < 0)
{
printf("Create socket failure: %s\n", strerror(errno)); //如果创建失败,打印错误信息
return -1;
}
printf("Socket create fd[%d]\n",listen_fd); //如果成功,返回fd
//服务器设置,设置server地址和端口号,用于接受任何client端
memset(&server_addr,0,sizeof(server_addr)); //清空再设置ipV4地址
server_addr.sin_family = AF_INET; //设置为ipV4地址
server_addr.sin_port = htons(port); //端口号,主机字节序转网络字节序,short是16位
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置host主机地址,INADDR_ANY是指定为0.0.0.0的地址,表示不确定地址,或指所有地址
// server_addr.sin_addr.s_addr = htonl(ip);
if( bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0 )
{
printf("Bind socket failure: %s\n", strerror(errno)); //如果绑定失败,打印错误信息
return -2;
}
printf("Socket bind ok \n");
listen(listen_fd,15); //等待连接,最大连接数设为15
printf("Socket listen ok \n");
while(1)
{
printf("Start accept %d...\n",listen_fd);
accept_fd = accept(listen_fd,NULL,NULL);
//accept接受的参数必须经过listen和bing处理过,返回一个新的socket码
//新的socket传送数据,旧的socket还能继续用accept接受请求
//后面两个参数设置为NULL,那么会在系统连接时自动根据客户端信息填入
if(accept_fd < 0)
{
printf("Accept socket failure: %s\n", strerror(errno)); //如果监听失败,打印错误信息
return -1;
}
printf("Socket Accept fd[%d]\n",accept_fd); //如果成功,返回fd
memset(buf, 0, sizeof(buf));
read(accept_fd,buf,sizeof(buf)); //从套接字read
printf("From client read : %s\n",buf);
write(accept_fd,"I have received the message.",strlen("I have received the message.")); //写回给套接字
sleep(1);
close(accept_fd);
}
close(listen_fd);
return 0;
} /* ----- End of main() ----- */
客户端程序
/*********************************************************************************
* Copyright: (C) 2018 Yujie
* All rights reserved.
*
* Filename: socket_client.c
* Description: client.c
*
* Version: 1.0.0(04/26/2018)
* Author: yanhuan <yanhuanmini@foxmail.com>
* ChangeLog: 1, Release initial version on "04/26/2018 07:16:03 PM"
*
********************************************************************************/
/*
1、创建套接字---socket
2、发送连接请求---connect
3、连接后,进行通信---read,write
4、释放套接字---close
*/
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define port 12000
#define buf_size 1024
#define ip 192.168.109.1
int main(int argc, char **argv)
{
int connect_fd = -1; //客户端只需要一个套接字:用来连接服务器
struct sockaddr_in server_addr; //指定服务器端地址,是一个结构体,等下要填充ipV4或者ipV6的结构
char buf[buf_size];
connect_fd = socket(AF_INET,SOCK_STREAM,0);//创建连接套接字,ipV4地址,可靠的面向连接的字符流,最后0是默认协议
if(connect_fd < 0)
{
printf("Create socket failure: %s\n", strerror(errno)); //如果创建失败,打印错误
return -1;
}
printf("Socket create fd[%d]\n",connect_fd); //如果成功,返回connect_fd
//连接服务器的准备,要连接的server地址和端口号
memset(&server_addr,0,sizeof(server_addr)); //清空再设置ipV4地址
server_addr.sin_family = AF_INET; //设置为ipV4地址
server_addr.sin_port = htons(port); //端口号,主机字节序转网络字节序,short是16位
//BSD网络软件包含inet_addr和inet_ntoa和inet_aton,用于二进制地址和‘.’分十进制字符表示,只适用于ipV4地址,即addr需要存放32位地址
//另外提供两个新函数:inet_ntop和inet_pton同时支持ipV4和ipV6,即addr需要存放128位地址
// inet_aton("ip",&server_addr.sin_addr);
inet_aton("127.0.0.1",&server_addr.sin_addr); //127.0.0.1被称为回环地址,默认被看成永不宕机的接口,不需要网卡就可ping同这个本地回环地址,一般用来检查本地网络协议、基本数据是否正常
//连接server端
if(connect(connect_fd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
{
printf("connect to server failure: %s\n", strerror(errno));
return -1;
}
write(connect_fd,"Hello!This message is from client.\n",strlen("Hello!This message is from client.\n"));
memset(buf,0,sizeof(buf));
read(connect_fd,buf,sizeof(buf));
printf("read '%s' from client\n",buf);
sleep(1);
close(connect_fd);
return 0;
}
注意:先开服务器程序(会阻塞),再开客服端程序。
本程序是最简单的TCP网络协议编程,服务器端一直等待,只能接受一个客户端信息,客户端只能运行一次。
解决上述的缺点,需要用到多路复用(select、poll、epoll)或多进程、多线程,将在下一阶段优化。