tcp协议 (可靠,一般用于文件传输)
tcp 协议特点:面向连接的,可靠的(相对于udp协议),流式服务
udp协议 (不可靠,但是实时性好)
TCP 编程流程
TCP 提供的是面向连接的、可靠的、字节流服务。
TCP 的服务器端和客户端编程流程如 下:

规定使用大端字节序列作为网络字节序列
主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。
大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。
小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。
Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换:
#include
uint32_t htonl(uint32_t hostlong); // 长整型的主机字节序转网络字节序
uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序
uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序
uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序
套接字地址——ip+端口
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
- #include
- struct sockaddr
- {
- sa_family_t sa_family;
- char sa_data[14];
- };
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对 应。常见的协议族和对应的地址族如下图所示:

TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,
它们分别用于 IPV4 和 IPV6:
sin_family:地址族 AF_INET
sin_port::端口号,需要用网络字节序表示
sin_addr: IPV4
地址结构:s_addr 以网络字节序表示 IPV4 地址
- struct in_addr
- {
- u_int32_t s_addr;
- };
-
- struct sockaddr_in//IPV4专用的,sockaddr 通用的
- {
- sa_family_t sin_family;
- u_int16_t sin_port;
- struct in_addr sin_addr;
- };
-
- struct in6_addr
- {
- unsigned char sa_addr[16]; // IPV6 地址,要用网络字节序表示
- };
-
- struct sockaddr_in6
- {
- sa_family_t sin6_family; // 地址族:AF_INET6
- u_inet16_t sin6_port; // 端口号:用网络字节序表示
- u_int32_t sin6_flowinfo; // 流信息,应设置为 0
- struct in6_addr sin6_addr; // IPV6 地址结构体
- u_int32_t sin6_scope_id; // scope ID,尚处于试验阶段
- };
设计定义定义时,socket 地址为通用的,传的时候可以是专有的
通常,人们习惯用点分十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换:
- #include
- in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址转化为网络字节序
- char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序转化为字符串表示
struct in_addr 里面是一个无符号整型

- #include
- #include

int socket(int domain, int type, int protocol);
socket() 创建套接字,成功返回套接字的文件描述符,失败返回-1
domain: 设置套接字的协议簇,
AF_UNIX AF_INET (刚开始大多使用这个,IPV4)AF_INET6(IPV6)
type: 设置套接字的服务类型 SOCK_STREAM(TCP协议选择流式服务) SOCK_DGRAM
protocol:一般设置为 0,表示使用默认协议
有了套接字就可以通过网络进行数据的收发。进行网络通信的程序首先就要创建一个套接字。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind() 将 sockfd 与一个 socket 地址绑定,成功返回 0,失败返回-1
sockfd 是网络套接字描述符
addr 是地址结构(见3.2),服务器的地址,客户端链接这个地址 struct sockaddr通用的
addrlen 是 socket 地址的长度
IP 地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整形值, 一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其 次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在 Linux 上,1024 以内的端口号,只有 root 用户可以使用。
int listen(int sockfd, int backlog);//这条语句一定不会阻塞,这只是代表创建一个位置,以便存放链接
listen() 创建一个监听队列以存储待处理的客户连接,成功返回 0,失败返回-1
sockfd 是被监听的 socket 套接字
backlog 表示处于完全连接状态的 socket 的上限
监听队列有两种,一个是存放未完成三次握手的连接, 一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept() 从 listen 监听队列中接收一个连接,成功返回一个新的连接 socket,
该 socket 唯一地标识了被接收的这个连接,失败返回-1
sockfd 是执行过 listen 系统调用的监听 socket
addr 参数用来获取被接受连接的远端 socket 地址(客户端地址)
addrlen 指定该 socket 地址的长度
accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则 accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
connect() 客户端需要通过此系统调用来主动与服务器建立连接,成功返回 0,失败返回-1 sockfd 参数是由 socket()返回的一个 socket
serv_addr 是服务器监听的 socket 地址,服务器的地址,服务器使用这个地址
addrlen 则指定这个地址的长度
connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方 法执行后,会进行三次握手, 建立连接。

int close(int sockfd);
close() 关闭一个连接,实际上就是关闭该连接对应的 socket
close()方法用来关闭 TCP 连接。此时,会进行四次挥手。
- 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);
recv() 读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小
send() 往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长度
flags 参数为数据收发提供了额外的控制
send()方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入到发送缓冲区中的数据长度。
recv()方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果 recv()返回值为 0, 说明对方已经关闭了 TCP 连接。

- ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags,struct sockaddr* src_addr, socklen_t *addrlen);
- ssize_t sendto(int sockfd, void *buff, size_t len, int flags,struct sockaddr* dest_addr, socklen_t addrlen);
recvfrom() 读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小 src_addr 记录发送端的 socket 地址
addrlen 指定该地址的长度
sendto() 往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长度 dest_addr 指定接收数据端的 socket 地址
addrlen 指定该地址的长度
Linux中使用 ifconfig 查看ip
windows中使用 ipconfig 查看ip
ser.c
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- int main()
- {
- int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
- if(sockfd == -1)
- {
- printf("create socket err\n");
- exit(1);
- }
-
- //IPV4
- //“127.0.0.1”回环地址,测试地址,谁写代表谁
- struct sockaddr_in saddr,caddr;//ipv4专用地址 saddr 服务器地址 caddr 客户端地址
- memset(&saddr,0,sizeof(saddr));//将地址空间内容清零,要先清空后续再填充
- saddr.sin_family=AF_INET;//地址族
- saddr.sin_port=htons(6000);//6000 端口号,4096以上临时端口可以使用,htons 转为网络字节序列
- saddr.sin_addr.s_addr=inet_addr("0.0.0.0");//将字符串转为无符号整型才可以使用 IPV4地址 “0.0.0.0”系统会自动识别ip地址,也可以直接给ip地址,若单纯只是测试,也可以使用回环地址“127.0.0.1”
-
- //命名,将sockfd与一个socket地址绑定
- int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//(struct sockaddr*)saddr 将IPV4专用地址改为通用地址
- if(res==-1)
- {
- printf("bind err\n");//失败的原因:端口被占用,地址错误....
- exit(1);
-
- }
-
- //创建监听队列
- res = listen(sockfd,5);
- if(res==-1)
- {
- printf("listen err\n");
- exit(1);
- }
-
- //接收(客户端连接)
- while(1)
- {
- int len=sizeof(caddr);//客户端地址长度
- int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//如果没有传入客户端地址,就会阻塞 成功返回一个新的链接 每个客户端过来连接服务器都会分配一个c
- if(c<0)
- {
- continue;
- }
- printf("ip:%s,port=%d\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));//ip 端口
- char buff[128]={0};
- int n = recv(c,buff,127,0);//接收数据,返回值为实际读到的字节数
- printf("recv(c=%d,n=%d)=%s\n",c,n,buff);
-
- send(c,"OK",2,0);//发送数据
- close(c);//关闭客户端连接
- }
- }
-
cli.c
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- int main()
- {
- //创建套接字
- int sockfd = socket(AF_INET,SOCK_STREAM,0);
- if(sockfd==-1)
- {
- printf("socket err\n");
- exit(1);
- }
- //发起连接,客户端主动发起连接,需要知道服务器的地址,就可以连接成功,就像打电话知道对方手机号码就可以成功,不局限于是哪个号码打的
- //客户端端口号系统随机分配
- struct sockaddr_in saddr;//服务器地址
- memset(&saddr,0,sizeof(saddr));
- saddr.sin_family=AF_INET;
- saddr.sin_port=htons(6000);
- saddr.sin_addr.s_addr=inet_addr("192.168.5.5");//这里的地址应该是服务器地址,
- int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//连接ip上的端口
- if(res==-1)
- {
- printf("connect err\n");
- exit(1);
- }
- //发送数据
- char buff[128]={0};
- printf("input:\n");
- fgets(buff,128,stdin);
-
-
- send(sockfd,buff,strlen(buff)-1,0);
-
- //接收反馈信息
- memset(buff,0,128);//清空buff中的数据,用来接收服务器发送的信息
- recv(sockfd,&buff,127,0);
- printf("buff=%s\n",buff);
- //关闭连接
- close(sockfd);
-
- }
-
-
要先运行服务器端,再运行客户端
多线程并发处理实现多台客户机同时连接服务器
在连接的时候产生一个线程 ,让线程去处理客户端
三次握手在传输层
三次握手用来建立连接

connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方 法执行后,会进行三次握手, 建立连接。

ser.c
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- int socket_init();
- void* fun();
-
- int main()
- {
-
- int sockfd=socket_init();
- if(sockfd==-1)
- {
- exit(1);
- }
-
- while(1)
- {
- pthread_t id;
- struct sockaddr_in caddr;
- int len=sizeof(caddr);
- int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
- if(c<0)
- {
- printf("accept err\n");
- exit(1);
- }
-
- printf("c=%d,ip:%s,port=%d\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
- pthread_create(&id,NULL,fun,(void*)c);
- }
- }
-
- void* fun(void* arg)//循环收发数据
- {
- int c = (int)arg;
- while(1)
- {
- char buff[128]={0};
- int n=recv(c,&buff,127,0);
- if(n<=0)//=0表示连接关闭
- {
- break;
- }
-
- printf("recv(c=%d)=%s\n",c,buff);
- send(c,"OK",2,0);
-
- }
- printf("close\n");
- close(c);
- }
-
- int socket_init()
- {
- int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
- if(sockfd==-1)
- {
- printf("socket err\n");
- exit(1);
- }
- struct sockaddr_in saddr;
- memset(&saddr,0,sizeof(saddr));//必须清零
- saddr.sin_family=AF_INET;
- saddr.sin_port=htons(6000);
- saddr.sin_addr.s_addr=inet_addr("0.0.0.0");
- int ser=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//将IPV4专用的地址强转为通用的
- if(ser==-1)
- {
- printf("bind err\n");
- exit(1);
- }
- ser=listen(sockfd,5);
- if(ser==-1)
- {
- printf("listen err\n");
- exit(1);
- }
- return sockfd;
-
- }
-
-
-
cli.c
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- int main()
- {
- int sockfd=socket(AF_INET,SOCK_STREAM,0);
- if(sockfd==-1)
- {
- printf("socket err\n");
- exit(1);
- }
- struct sockaddr_in saddr;//服务器地址
- memset(&saddr,0,sizeof(saddr));
- saddr.sin_port=htons(6000);
- saddr.sin_family=AF_INET;
- saddr.sin_addr.s_addr=inet_addr("192.168.43.71");
-
- //开始三次握手,connect返回成功,则三次握手完成
- int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
- if(res==-1)
- {
- printf("connect err\n");
- exit(1);
- }
- while(1)
- {
- char buff[128]={0};
- printf("input:\n");
- fgets(buff,128,stdin);
- if(strncmp(buff,"end",3)==0)
- {
- break;
- }
-
- send(sockfd,buff,strlen(buff)-1,0);
-
- //接收反馈信息
- memset(buff,0,128);//清空buff中的数据,用来接收服务器发送的信息
- recv(sockfd,&buff,127,0);
- printf("buff=%s\n",buff);
-
- }
- close(sockfd);
-
- }
-
-


- void* fun(void* arg)//循环收发数据
- {
- int c = (int)arg;
- while(1)
- {
- char buff[128]={0};
- int n=recv(c,&buff,1,0);//每次只接收一个字符
- if(n<=0)//=0表示连接关闭
- {
- break;
- }
-
- printf("recv(c=%d)=%s\n",c,buff);
- send(c,"OK",2,0);
-
- }
- printf("close\n");
- close(c);
- }
-

查看缓冲区看是否有数据

说明剩下的的4个OK都发送过来了,在客户端的缓冲区中,只是没有机会读取出来
但是我们客户端读取数据的时候可以读取127个字符,为什么只读到了2个呢?
程序运行结果说明当时客户端读缓冲区时只有两个字符,剩下的(4个OK)8个字符还没过来,所以recv就只将有的读走了,接着程序就运行到input等待输入字符,没有机会读取缓冲区

查看缓冲区



ser.c
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- int socket_init();
- void fun();
-
- int main()
- {
-
- int sockfd=socket_init();
- if(sockfd==-1)
- {
- exit(1);
- }
-
-
- while(1)
- {
- pid_t id=fork();
- struct sockaddr_in caddr;
- int len=sizeof(caddr);
- int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
- if(c<0)
- {
- printf("accept err\n");
- exit(1);
- }
-
- printf("c=%d,ip:%s,port=%d\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
- fun(&c);
- }
- }
-
- void fun(int *arg)//循环收发数据
- {
- int c=*arg;
- while(1)
- {
- char buff[128]={0};
- int n=recv(c,&buff,127,0);//每次只接收一个字符
- if(n<=0)//=0表示连接关闭
- {
- break;
- }
-
- printf("recv(c=%d)=%s\n",c,buff);
- send(c,"OK",2,0);
-
- }
- printf("close\n");
- close(c);
- }
-
- int socket_init()
- {
- int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
-
- if(sockfd==-1)
- {
- printf("socket err\n");
- exit(1);
- }
- struct sockaddr_in saddr;
- memset(&saddr,0,sizeof(saddr));//必须清零
- saddr.sin_family=AF_INET;
- saddr.sin_port=htons(6000);
- saddr.sin_addr.s_addr=inet_addr("0.0.0.0");
- int ser=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//将IPV4专用的地址强转为通用的
- if(ser==-1)
- {
- printf("bind err\n");
- exit(1);
- }
- ser=listen(sockfd,5);
- if(ser==-1)
- {
- printf("listen err\n");
- exit(1);
- }
- return sockfd;
-
- }
-