TCP协议由于其传输的稳定性,在很多程序中都在使用。本文主要记录TCP网络编程的基础知识,包括其编程过程中使用的套接字的基础知识和其编程的流程。
套接字基础
既然要进行TCP下的网络编程,那么其中肯定就会涉及到网络地址的使用,那么就不可避免的会使用到套接字定义的地址结构。
套接字地址结构
套接字编程需要指定套接字的地址作为参数,不同的协议族有着不同的地址结构定义方式。这些地址结构通常以sockaddr_开头,每一个协议族有一个唯一的后缀,常用的sockaddr_in就是以太网的地址结构名称。
通用的套接字地址类型定义如下,它可以在不同协议族之间进行强制转换。
struct sockaddr
{
sa_family_t sa_family; //协议族
char sa_data[14]; //协议族数据
};
在实际使用中,几乎所有的套接字函数都会用到这个地址结构作为参数,但是这个struct sockaddr结构体不方便设置,通常使用struct sockaddr_in来进行地址结构的定义:
struct sockaddr_in
{
u8 sin_len;
u8 sin_family;
u16 sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
可以看到这个地址结构是一个结构体里包含着另一个结构体的结构,其中的结构体struct in_addr结构体用于表示IP地址,其定义为:
struct in_addr
{
u32 s_addr; //32位IP地址
};
至此,整个sockaddr_in的地址结构定义完毕,其中sin_len是结构体struct sockaddr_in的长度,16个字节,sin_family表示协议家族,通常指定为AF_INET,sin_port是一个16的端口号,而结构体型变量sin_addr则表示一个32为的IP地址,最后的sin_zero[8]暂时未使用,保留。
TCP网络编程流程
TCP编程主要为C/S模式,即服务器客户端模式。对于服务器而言,其先创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后 ,根据用户的请求进行处理;客户端这边,根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。服务器模式与客户端模式的编程流程如下图所示:
服务器端模式的编程流程主要分为套接字初始化(socket())、套接字与端口的绑定(bind())、设置服务器的监听连接(listen())、接受客户端连接(accept())、接收和发送数据(read()和write())并进行数据处理及处理完毕之后的对套接字的关闭(close())。
套接字初始化过程中,根据用户对套接字的需求来确定套接字的选项。这个过程中的使用的函数为socket(),它按照用户定义的网络类型、协议类型和具体的协议标号等参数来定义。系统根据用户的需求生成一个套接字文件描述符供用户使用。
套接字与端口的绑定过程中,将套接字与一个地址结构进行绑定。绑定之后,在进行网络程序设计的时候,套接字所代表的IP地址和端口地址及协议类型等参数按照绑定值进行操作。
由于一个服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理有限个数的客户端连接请求,所以服务器需要设置服务端排队队列的长度。服务器监听连接会设置这个参数,限制客户端中等待服务器处理连接请求的队列长度。
在客户端发送连接请求之后,服务器需要接收客户端的连接,然后才能进行其他的处理。
在服务器接收客户端连接请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果发送给客户端。
当服务器处理完数据,要结束与客户端的通信过程时,需要关闭套接字的连接。
客户端模式的编程流程主要分为套接字初始化(socket())、连接服务器(connect())、读写网络数据(read()、write())并进行数据处理及处理完毕后的关闭套接字(close())。
创建网络插口函数socket()
int socket(int domain,int type,int protocol);
功能:创建网络插口,获得文件描述符
参数:
domain:协议族,用于设置网络通信的域,socket()根据这个参数选择通信协议的协议族,这些协议族文件在
名称 |
含义 |
名称 |
含义 |
PF_UNIX,PF_LOCAL |
本地通信 |
PF_X25 |
ITU-T X25/ISO-8208协议 |
PF_INET |
Ipv4 Internet协议 |
PF_AX25 |
Amateur radio AX25协议 |
PF_INET6 |
Ipv4 Internet协议 |
PF_ATMPVC |
原始ATMPVC访问 |
PF_IPX |
IPX-Novell协议 |
PF_APPLETALK |
Appletalk |
PF_NETLINK |
内核用户界面设备 |
PF_PACKET |
底层包访问 |
type:协议类型,用于设置套接字通信的类型,常用的主要有SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报套接字)等。
名称 |
含义 |
SOCK_STREAM |
TCP连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输 |
SOCK_DGRAM |
支持UDP连接 |
SOCK_SEQPACKET |
序列化包,提供一个序列化的、可靠的、双向的基于连接的数据传输通道,数据长度定长,提供原始网络协议访问 |
SOCK_RAW |
RAW类型,提供原始网络协议访问 |
SOCK_RDM |
提供可靠的数据报文,不过可能数据会乱序 |
SOCK_PACKET |
专用类型,直接从设备驱动接收数据,不能在通用程序中使用 |
类型为SOCK_STREAM的套接字表示一个双向的字节流,与管道类似。流式套接字在进行数据收发之前必须已经使用coonect()函数连接。一旦连接,就可以使用read()函数或write()函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接收,当数据在一段时间内仍然没有接收完毕,可以认为这个连接已经死掉了。
SOCK_DGRAM和SOCK_RAW这两种套接字可以使用函数sendto()函数来发送数据,使用recvfrom()函数来接收数据,recvfrom()接收来自指定IP地址的发送方的数据。
protocol:协议编号,用于指定某个协议的特定类型,即type类型中的某个类型。通常某个协议中只有一种特定类型,这样protocol只能设置为0,但是有些协议有多种特定的类型,就需要设置此参数来选择特定的类型。
返回值:成功,返回一个表示这个套接字的文件描述符;失败,返回-1,错误值有如下几种:
错误值 |
含义 |
EACCES |
没有权限建立指定协议家族的类型的socket |
EAFNOSUPPORT |
不支持所给的地址类型 |
EINVAL |
不支持此协议或者协议不可用 |
EMFILE |
进程文件表溢出 |
ENFILE |
打开文件过多 |
ENOBUFS/ENOMEM |
内存不足 |
EPROTONOSUPPORT |
指定的协议类型type在协议家族domain中不存在 |
其他 |
其他 |
实际使用时,常采用如下赋值方式:
int sock = socket(AF_INET,SOCK_STREAM,0);
绑定地址端口对函数bind()
int bind(int sock,struct sockaddr *addr,socklen_t addrlen);
功能:将长度为addrlen的struct sockaddr类型的地址addr与sock绑定到某个端口上。
参数:
sock,socket()函数的返回值;
addr,指向一个结构为sockaddr的指针,其中结构体sockaddr中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要先将地址结构中的IP地址、端口、类型和结构体sockaddr中的域进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等结合在一起。
addrlen,addr的长度,可以设置成sizeof(struct sockaddr)。
返回值:成功,返回0;失败,返回-1。错误值有如下几种:
AF_UNIX错误值 |
含义 |
备注 |
EADDRINUSE |
给定地址已经被使用 |
|
EBADF |
sock不合法 |
|
EINVAL |
sock已经被绑定到其他地址 |
|
ENOTSOCK |
sock不是socket描述符 |
|
EACCES |
地址被保护,权限不够 |
|
EADDRNOTAVAIL |
接口不存在或者绑定地址不是本地 |
UNIX协议族,AF_UNIX |
EFAULT |
addr指针超出用户空间 |
UNIX协议族,AF_UNIX |
EINVAL |
地址长度错误,或socket不是AF_UNIX族 |
UNIX协议族,AF_UNIX |
ELOOP |
解析addr时符号链接过多 |
UNIX协议族,AF_UNIX |
ENAMETOOLONG |
addr长度过长 |
UNIX协议族,AF_UNIX |
ENOENT |
文件不存在 |
UNIX协议族,AF_UNIX |
ENOMEM |
内存内核不足 |
UNIX协议族,AF_UNIX |
ENOTDIR |
不是目录 |
UNIX协议族,AF_UNIX |
EROFS |
Socket节点未在只读文件系统上 |
UNIX协议族,AF_UNIX |
实际使用时,可按照如下方法定义:
struct sockaddr_in addr;
bind(sock,(struct sockaddr*)&addr,sizeof(addr));
监听本地端口函数listen()
int listen(int sock,int backlog);
功能:初始化服务器可连接队列。因为服务器处理客户端连接请求时是顺序处理的,同一时间仅能处理一个客户端连接,当多个客户端的连接请求同时到来的时候,服务器并不是同时处理的,而是将不能处理的客户端连接请求放到等待队列中。
参数:
sock,监听的描述符,为socket()函数的返回值;
backlog,等待队列的长度,实际使用时可自主赋值,大多数系统的设置值为20,我们可以将其设置为5或者10。
返回值:成功,返回0;失败,返回-1,错误值为:
错误值 |
含义 |
EADDRINUSE |
另一个socket已经在同一端口监听 |
EBADF |
参数sock非法 |
ENOTSOCK |
sock不是代表socket的文件描述符 |
EOPNOTSUPP |
不支持listen()操作 |
ECONNREFUSED |
等待队列已满 |
listen()函数仅对类型为SOCK_STREAM或者SOCK_SEQPACKET的协议有效。
实际使用时一般这样定义:
int listen(sock,5);
接受连接请求函数accept()
int accept(int sock,struct sockaddr *addr,socklen_t *addrlen);
功能:接受客户端的连接请求
参数:
sock:socket()函数返回的描述符
addr:指向一个结构为sockaddr的指针,其中结构体sockaddr中包含了客户端地址、端口和IP地址的信息。
addrlen:表示第二个参数addr所指的内容的长度,可以使用sizeof()函数来获得。
返回值:函数执行成功后会返回一个新的套接口文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新描述符来获得。因此,当服务器成功处理客户端的请求连接后,会有两个文件描述符,旧的文件描述符表示正在监听的socket,新产生的文件描述符表示客户端的连接,函数send()和recv()通过新的文件描述符进行数据的收发。如果执行失败,函数返回-1,并通过errno得到错误值:
错误值 |
含义 |
EAGAIN/EWOULDBLOCK |
此socket使用了非阻塞模式,当前情况下没有可接受的连接 |
EBADF |
描述符非法 |
ECONNABORTED |
连接取消 |
EINTR |
信号在合法连接到来之前打断了accept的系统调用 |
EINVAL |
socket没有监听连接或者地址长度不合法 |
EMFILE |
每个进程允许打开的文件描述符数量已经到达最大值 |
ENFILE |
达到系统允许打开文件的总数量 |
ENOTSOCK |
文件描述符是一个文件,不是socket |
EOPNOTSUPP |
引用的socket不是流类型SOCK_STREAM |
EFAULT |
参数addr不可写 |
ENOBUFS/ENOMEM |
内存不足 |
EPROTO |
协议错误 |
EPERM |
防火墙不允许连接 |
实际使用时,可按照如下方法使用:
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
accept(sock,(struct sockaddr*)&clientaddr,&len);
连接目标网络服务器函数connect()
int connect(int sock,struct sockadd *addrser,int addrlen);
功能:将客户端连接到指定参数的服务器上。
参数:
sock:客户端使用socket()函数建立套接字时返回的套接字文件描述符
addrser:一个指向结构体sockaddr的指针,其中包括客户端需要连接的服务器的目的端口和IP地址,以及协议类型
addrlen:表示第二个参数的内容的大小,可使用sizeof()函数获得,与bind()函数不同,这个参数是一个变量而不是指针
返回值:成功,返回0;失败,返回-1,通过errno获得错误值:
错误值 |
含义 |
EACCES |
目录不可读或者不可写 |
EACCES/EPERM |
用户没有设置广播标志而连接广播地址或者连接请求被防火墙限制 |
EADDRINUSE |
本地地址已经在使用 |
EAFNOSUPPORT |
参数addrser的协议类型设置不正确 |
EAGAIN |
本地端口不足 |
EALREADY |
socket是非阻塞类型并且前面的连接没有返回 |
EBADF |
文件描述符不是合法的值 |
ECONNREFUSED |
连接的主机地址没有监听 |
EFAULT |
socket结构地址超出用户空间 |
EINPROGRESS |
socket是非阻塞模式,而连接不能立刻返回 |
EINTR |
函数被信号中断 |
EISCONN |
socket已经连接 |
ENETUNREACH |
网络不可达 |
ENOTSOCK |
文件描述符不是一个socket描述符 |
ETIMEOUT |
连接超时 |
因为connect()函数是客户端模式下的函数,所以其需要的地址结构、端口等都需要重新定义:
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addrser("127.0.0.1");
addr.sin_port = htons(6888);
int connect(sock, (struct sockaddr*)&addrser, sizeof(addrser));
写入数据函数write()
i
int size;
char buffer[1024]={0};
int size = int write(sock,buffer,len);
功能:将缓冲区的内容全部写入套接字文件描述符sock中
参数:
sock:无论是服务器还是客户端,sock都是其socket()函数的返回值
buffer:此处指内容,即放到缓冲区里的内容
len:一般为缓冲区的长度 ,可通过sizeof()获得
返回值:成功写入的数据的长度
读取数据函数write()
int size;
char buffer[1024]={0};
int size = int read(sock,buffer,len);
功能:从套接字描述符sock读取len长度的内容全部放入缓冲区中
参数:
sock:无论是服务器还是客户端,sock都是其socket()函数的返回值
buffer:此处指内容,即放到缓冲区里的内容
len:一般为缓冲区的长度 ,可通过sizeof()获得
返回值:成功读取的数据的长度
关闭套接字函数close()
close(sock);
关闭已经打开的socket连接,内核会释放相关资源。sock为socket()函数创建的套接字描述符。关闭套接字之后就不能再使用这个套接字描述符来进行读写操作了。
除此之外,还可使用shutdown()函数来允许单方向切断通信或者切断双方的通信。
int shutdown(int sock,int how);
功能:关闭已打开的socket连接,单方向切断通信或切断双方的通信。
参数:
sock:要切断通信的socket文件描述符,即为socket()函数创建的socket描述符
how:表示切断的方式,取值如下:
SHUT_RD:值为0,表示切断读,之后不能再使用此描述符进行读操作。
SHUT_WR:值为1,表示切断写,之后不能再使用此描述符进行写操作。
SHUT_RDWR:值为2,表示切断读写,之后不能再使用此描述符进行读或写操作。
返回值:成功,返回0;失败,返回-1,并通过errno获得错误值:
错误 |
含义 |
EBADF |
文件描述符非法 |
ENOTSOCK |
sock不是socket描述符 |
ENOTCONN |
socket没有连接 |
案例:客户端连接服务器后从标准输入读取输入的字符串,发送给服务器,服务器接收到字符串后,发送接收到的总字符串个数给客户端,客户端将接收到的服务器的消息打印输出。
server.c
对于服务器端而言,其编程流程为建立套接字、初始化网络地址、绑定、监听、接收客户端连接请求、收发数据、关闭套接字。
#define PORT 5500
int main()
{
//创建socket
int s_sock = socket(AF_INET,SOCK_STREAM,0);
if(-1 == s_sock)
{
perror("socket");
return -1;
}
//初始化socket地址结构,设置服务器地址
struct sockaddr_in serveraddr; //服务器地址结构
bzero(&serveraddr,sizeof(serveraddr)); //清0
serveraddr.sin_family = AF_INET; //协议族
serveraddr.sin_port = htons(PORT); //服务器端口
serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //本地地址
//绑定
int rbind = bind(s_sock,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
if(-1 == rbind)
{
perror("bind");
return -1;
}
//监听
int rlisten = listen(s_sock,10); //设置监听队列长度为10
if(rlisten == -1)
{
perror("listen");
close(s_sock);
return -1;
}
printf("等待客户端连接......\n");
struct sockaddr_in clientaddr; //客户端地址结构
socklen_t len=sizeof(clientaddr);
while(1)
{
fflush(stdout);
//接受客户端连接请求,返回socket描述符
int c_sock = accept(s_sock,(struct sockaddr*)&clientaddr,&len);
if(c_sock == -1)
{
perror("accept");
return -1;
}
else
{
printf("Client %s %u:已连接......\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
}
//建立一个新的进程来处理新到来的连接
pid_t pid;
pid = fork();
if(pid == 0)
{
//在子进程中
close(s_sock); //关闭服务器的监听
//处理数据
char buffer[1024]; //存放数据的缓冲区
while(1)
{
int size = read(c_sock,buffer,1024);
if(0 == size)
{
printf("客户端关闭连接......\n");
break;
}
else if(-1 == size)
{
perror("read");
break;
}
//输出接收到的数据
printf("Client:%s",buffer);
//发送数据
sprintf(buffer,"发送数据%d字节!\n",size);
write(c_sock,buffer,strlen(buffer) + 1); //发送给客户端
}
}
else
{
close(c_sock);
}
}
return 0;
}
client.c
对于客户端而言,创建socket之后就可以直接进行连接,之后再进行数据的处理。
int main()
{
//创建socket描述符
int c_sock = socket(AF_INET,SOCK_STREAM,0);
if(c_sock == -1)
{
perror("socket");
return -1;
}
//设置服务器地址结构
struct sockaddr_in serveraddr;
bzero(&serveraddr,sizeof(serveraddr)); //清0
serveraddr.sin_family = AF_INET; //协议族
serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //本地地址
serveraddr.sin_port = htons(5500);
//连接服务器
int ret = connect(c_sock,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
if(ret == -1)
{
perror("connect");
return -1;
}
//收发数据
int size = 0;
char buffer[1024];
while(1)
{
printf("input:");
fflush(stdout);
size = read(0,buffer,sizeof(buffer)); //从标准输入中读取数据放到缓冲区
write(c_sock,buffer,sizeof(buffer)); //发送给服务器
size = read(c_sock,buffer,sizeof(buffer)); //从服务器读取数据
if(size == -1)
{
perror("read");
continue;
}
else if(size == 0)
{
printf("服务器关闭连接......\n");
break;
}
else
{
write(1,buffer,size);
memset(buffer,0x00,1024);
}
}
shutdown(c_sock,SHUT_RDWR);
close(c_sock);
return 0;
}