对于上一个博客我大致讲解了网络的基本概念与相关协议,这篇博客我给大家介绍传输层的网络编程流程基础。
怎样完成网络编程
在网络应用进程通信时,最主要的进程间的交互的模型是客户/服务器(C/S)模型。
服务器: 提供数据的为服务器
客户端: 获取数据的为客户端
在传输层我们常用的两种协议是TCP和UDP,TCP是面向连接的、可靠的、流式服务。UDP: 无连接、不可靠的、数据报服务。
TCP的编程流程
TCP 的编程流程
server(服务器):socket bind listen accept recv send close
client(客户端): socket connect send recv close
首先对于服务器:
(1)socket创建网络套接字
#include<sys/socket.h>
#include<sys/types.h>
int socket(int domain,int type,int protocol);
//成功返回一个socket文件描述符,失败返回-1
domain:告诉系统使用那个底层协议族,对于TCP/IP协议族而言,参数设置为AF_INET/PF_INET(用于IPV4)或者PF_INET6(用于IPV6);对于UNIX本地域协议族而言,参数设置为PF_UNIX等,我们基本设置为ipv4即AF_INET
type:指定服务类型,服务类型主要有SOCK_STREAM服务(流式服务)和SOCK_DGRAM(数据报服务)。所以对于TCP/IP协议族来说我们如果选取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传输层使用UDP协议。
protocol:在前两个参数构成的协议集合下,再选择一个具体的协议。我们几乎都会将其设置为0,表示使用默认协议
ps:socket链接是全双工滴。。
(2)bind绑定socket与地址(IP+端口),也称命名socket
对于上面创建了socket指定了使用那个地址族,但是并未指定使用该地址族中的那个具体的socket地址,对于服务器来说,只有命名了socket,将服务器的ip及其端口和socket绑定,才能将其发布出去,客户端才能知道如何连接它。相当于用socket实现进程与进程的通信。
为什么客户端不需要这个bind操作呢?
因为服务器是一个大型的pc,它往往是固定的ip与其端口,它不能一直变化,那么客户端就没有办法请求它(比如百度的服务器,它的ip地址和端口就是固定绑定滴)。但是呢,对于客户端来说就没有必要绑定其socket与其固定的ip和端口,不会有其他客户端会一直请求访问你这个客户端,客户端会使用操作系统自动分配的地址,即采用匿名方式。另外一点,因为客户端总是发起链接的一方,所以服务器能获得客户端的信息,所以在客户端这一方可以不使用bind函数。
#include<sys/socket.h>
#include<sys/types.h>
int bind(int sockfd,const struct sockaddr *my_addr,socklen_t addrlen);
//成功返回0,失败返回-1
sockfd:未命名的文件描述符,将要绑定的sockfd
my_addr:指定要绑定的ip与端口号(socket地址)
addrlen:指定该socket地址的长度,即my_addr的长度
对于my_addr我们可以看出其是一个结构体,那么ip地址与端口号在这个结构体中如何定义的呢?TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,针对于IPV4和IPV6,我常用IPV4所以只列出sockaddr_in的结构
struct sockaddr_in
{
sa_family_t sin_family; //地址族:AF_INET
u_int16_t sin_port; //端口号,要用网络字节序表示,unsigned_int 16位
struct in_addr sin_addr; //IPV4地址结构体,下面
}
struct in_addr
{
u_int32_t s_addr; //IPV4的IP地址,网络字节序表示, unsigned_int 32位
}
对于这个通用socket地址类型sockaddr,我们这个专用sockaddr_in需要强制转化。
端口号转化
因为网络字节序是大端模式,PC是小端模式(inter的基本都是,AMD基本是大端),所以需要把主机的小端字节序转换成大端字节序,我们就需要相应的函数将其转换。Linux提供了两个主机转网络的转化函数。
#include<netinet/in.h>
unsigned short int htons(unsigned short int hostshort); //host to network short
unsigned long int htonl(unsigned long int hostlong); //host to network long
//在这两个函数中,长整形函数通常用于IP地址的转换,短整型函数用来转换端口号
htons表示“host to network short“,即短整形的主机字节序数转化成网络字节序数据,这是我们常用的转化端口的函数,重要。
IP地址转化
我们习惯用可读性好的字符串表示IP地址,比如点分十进制字符串表示IPV4地址,但是在编程中我们需要把IP地址转化为整形(二进制)才能使用。Linux依旧给我们提供了两个函数,一起看看。
#include<arpa/inet.h>
in_addr_t inet_addr(const char*strptr); //将点分十进制字符串表示的IPV4地址转化为网络字节序整数表示的IPV4地址,失败返回INADDR_NONE
int inet_aton(const char *cp,struct in_addr *inp); //与inet_addr功能相同,将转化结果存储于参数inp指向的结构中
方便起见,我们常用inet_addr,成功则返回将用点分十进制字符串表示的IPV4地址转化为用网络字节序整数表示的IPV4地址。
bind函数调用失败只可能是端口号被占用或者IP地址出错
(3)监听socket
socket被命名后,不能马上接收客户连接,我们需要使用如下系统调用创建一个监听队列以存放待处理的客户连接(实际上,listen函数只是启动监听,真正维护监听队列的都是内核操作,内核会将这些正在连接的和已连接的客户端进行相应处理,所以在listen函数并不会阻塞,它只做启动监听告诉内核这个步骤)
#include<sys/socket.h>
int listen(int sockfd,int backlog);
//成功返回0,失败返回-1
sockfd:指被监听的socket
backlog:提示内核监听队列的最大长度,典型值为5。
监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到错误信息。
ps:在Linux内核2.2之前,backlog指所有处于半连接状态和完全连接状态的客户端socket的上限,(即内核只维护了一个监听队列)。但在Linux内核2.2之后,socket backlog
参数的形为改变了,现在它指等待accept
的完全建立
的套接字的队列长度。 处于半连接状态的socket上限可以使用/proc/sys/net/ipv4/tcp_max_syn_backlog
设置(即内核维护两个队列:具有由系统范围设置指定的大小的半连接队列
和 应用程序(也就是backlog参数)指定的accept
队列。)因为它是从0号开始计算,backlog是最大下标数,所以内核中已连接的socket最大往往是6(设定backlog为5,即5+1)。
看下我系统上默认的SYN
队列(半连接状态上限,系统默认)大小:
(4)接受连接
用下面系统调用从listen监听队列中接收一个连接
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//成功返回新的连接的socket,失败返回-1并设置errno
//ps:socklen_t指的是int
sockfd:执行过listen系统调用的监听socket
addr:连线成功时,参数addr所指的结构会被系统填入客户端的 IP 地址和端口号(客户端的socket地址)
addrlen:参数addrlen为scokaddr_in的结构长度,传递指针是系统最终会将实际值赋给addrlen所指向的值。
为什么需要再返回一个socket呢?
因为返回的这个socket是唯一的标识了被接受的这个连接,服务器可以通过读写该socket与连接的客户端进行通讯,我们不能用原来的那一个socket来为我们本次连接进行服务,因为不止一个客户端会连接服务器,那么我们如果用原来的那个socket,它岂不是忙不过来了,所以对于每一个客户端连接,服务器都会有相应的一个socket为每次的连接服务,而不会使用原来的socket进行服务。原来那个socket是用来监听端口的,看看谁和这个服务器需要连接。真正为某一次客户端连接进行服务的就是各自返回的这个socket了。
总的概括就是socket函数返回的sockfd只是一个让客户端链接的一个标识描述符,真正对于某一具体的客户端链接对其操作的是accept返回的描述符。原socket只是引导,不提供服务。
(5)数据读写
实际read/write同样适用于socket,但是socket编程接口提供了专门用于socket数据读写的系统调用,增加了对数据的读写控制
#include<sys/types.h>
#include<sys/socket.h>
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);//成功返回实际写入的长度
//失败都返回-1
recv读取sockfd上的数据存入buff,每次读取的个数用len表示
send往sockfd上写入数据,从buff中获取len个字节写入sockfd中
参数flags是为数据收发提供额外的控制,通常设置为0。
(6)发起连接(客户端)
服务器通过listen调用来被动接收连接,客户端需要通过connect来主动与服务器建立连接
#include<sys/types.h>
#include<sy/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
//成功返回0,失败返回-1设置errno
sockfd:由socket系统调用返回一个socket用于通讯(标识进程)
serv_addr:表示要连接的服务器的 IP 地址和端口号,即服务器监听的socket地址
addrlen:指定连接服务器的地址长度
ps:一旦成功建立连接,客户端就可以用这个sockfd与服务器进行通讯,这个sockfd唯一的标识了这个连接,无需像服务器那样单独一个sockfd文件描述符进行数据处理,因为也没有其他的客户端去连接客户端,都是与服务器进行交互的。
(7)关闭连接
关闭一个连接实际上就是关闭对应的socket
#include<unistd.h>
int close(int fd);
//成功返回0,失败返回-1
TCP编程实例代码:
服务器端:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in ser,cli;
ser.sin_family=AF_INET;
ser.sin_port=htons(6000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");//不需要网络都可以访问本机,回环地址
int res=bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res!=-1);
listen(sockfd,5);
while(1) //为了让服务器不退出,一直可以接收客户端的连接
{
int len=sizeof(cli);
int c=accept(sockfd,(struct sockaddr*)&cli,&len); //相当于来电显示,客户端发起链接服务器就可以获取到你的信息,内核自动填充到cli,返回值返回的是维护本次链接的文件描述符
if(c<0) //连接失败,重新连接其他的客户端
{
printf("error\n");
continue;
}
while(1)
{
char recvbuff[128]={0};
int n=recv(c,recvbuff,127,0); //recv会阻塞,等待客户端发送,客户端关闭recv也会返回
if(n<=0)
{
printf("link over\n");
break;
}
printf("%d:%s\n",c,recvbuff);
send(c,"ok",2,0);
}
close(c); //关闭与某一个客户端的连接
}
close(sockfd); //关闭服务器,实际上服务器一般是不关滴
}
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in ser,cli;
memset(&ser,0,sizeof(ser));
ser.sin_family=AF_INET; //初始化要与之进行通讯的服务器socket地址
ser.sin_port=htons(6000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=connect(sockfd,(struct sockaddr*)&ser,sizeof(ser));//发起与服务器的连接
assert(res!=-1);
while(1)
{
printf("please input:");
fflush(stdout);
char buff[128]={0};
fgets(buff,127,stdin);
buff[strlen(buff)-1]=0; //fgets会自动加回车
if(strcmp(buff,"end")==0)
break;
send(sockfd,buff,strlen(buff),0);
memset(buff,0,sizeof(buff));
recv(sockfd,buff,2,0);
printf("%s\n",buff);
}
close(sockfd); //断开与服务器连接
}
ps:在服务器中close需要用两次,即一次关闭连接,一次关闭socket。
我们这个程序可以一个服务器多次处理不同客户端的链接,不退出,但是不能同时处理多个客户端请求,关于同时处理之后详解