1、TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下:
(1)TCP服务端编程的一般步骤为:
(2)TCP客户端端编程的一般步骤为:
注:
(1)套接字
TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字(socket)或插口。
套接字用(IP地址:端口号)表示。
它是网络通信过程中端点的抽象表示,包含进行网络通信必需的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
(2)套接口
通讯的基石是套接口,一个套接口是通讯的一端。在这一端上你可以找到与其对应的一个名字。一个正在被使用的套接口都有它的类型和与其相关的进程。套接口存在于通讯域中。通讯域是通过套接口通讯来处理一般的线程而引进的一种抽象概念。
(3)错误处理:包裹函数
包裹函数调用实际函数,检查返回值,发生错误终止进程。确定包裹函数名的约定是大写实际函数名的第一个字符。
(4)三次握手
a、对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。
通常的情况,客户端的 connect() 函数默认会一直阻塞,直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。
b、listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长度,TCP 三次握手也不是由这个函数完成,listen()的作用仅仅告诉内核一些信息。
这里需要注意的是,listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。
这样的话,当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,将建立好的链接自动存储到队列中,如此重复。所以,只要 TCP 服务器调用了 listen(),客户端就可以通过 connect() 和服务器建立连接,而这个连接的过程是由内核完成。
c、accept()函数功能是,从处于 established (建立)状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。
2、UDP通信模型
3、网络地址结构struct in_addr 和struct sockaddr和struct sockaddr_in
struct sockaddr和struct sockaddr_in这两个结构体用来处理网络通信的地址。
(1)in_addr
#include
struct in_addr {
in_addr_t s_addr;
};
功能:表示一个32位的IPv4地址
in_addr_t 一般为 32位的unsigned int,其字节顺序为网络顺序(network byte ordered),即该无符号整数采用大端字节序。
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
(2)sockaddr
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
sockaddr在头文件#include
sin_family:表示地址类型,对于使用TCP/IP协议进行的网络编程,该值只能是AF_INET
sa_data:存储具体的协议地址
(3)sockaddr_in(最常用)
sockaddr_in在头文件#include
或#include
中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:
(4)bzero
#include
extern void bzero(void *s, int n);
功能:置字节字符串s的前n个字节为零且包括‘\0’。
参数:
s 要置零的数据的起始地址;
n 要置零的数据字节个数。
例:bzero(&server_addr,sizeof(server_addr));
初始化服务器地址
另:void *memset(void *s, int ch, size_t n);
功能:将s中前n个字节替换为ch并返回s;
(5)struct sockaddr_in和struct sockaddr区别
二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
4、socket()
#include
int socket( int af, int type, int protocol);
功能:用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数。
返回值:socket(>0) 成功; INVALID_SOCKET(-1) 错误 错误代码存入errno中
af:一个地址描述。目前仅支持AF_INET格式,也就是说ARPA Internet地址格式。
type:新套接口的类型描述。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
常用的协议有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注:socket返回的值是一个文件描述符,SOCKET类型本身也是定义为int的,既然是文件描述符,那么在系统中都当作是文件来对待的,0,1,2分别表示标准输入、标准输出、标准错误。所以其他打开的文件描述符都会大于2, 错误时就返回 -1. 这里INVALID_SOCKET 也被定义为 -1。
例:
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
注: 回送地址:127.0.0.1 一般用于网络软件测试以及本地机进程间通信
创建一个套接口。
5、bind()
功能:将一本地地址与一套接口捆绑。本函数适用于未连接的数据报或流类套接口,在connect()或listen()调用前使用。当用socket()创建套接口后,它便存在于一个名字空间(地址族)中,但并未赋名。bind()函数通过给一个未命名套接口分配一个本地名字来为套接口建立本地捆绑(主机地址/端口号)。
返回值:成功后返回0,当有错误发生时则返回-1,错误代码存入errno中。
注:客户在调用函数connect之前不一定要调用bind()函数,因为有必要的话,内核会选择源IP地址和一个临时端口。
(1)在windows 环境下为
#include
int PASCAL FAR bind( SOCKET sockaddr, const struct sockaddr FAR* my_addr,int addrlen);
(2)在Linux环境为:
#include
#include
int bind( int sockfd , const struct sockaddr * my_addr, socklen_t addrlen);
参数:
sockfd: 表示已经建立的socket编号(描述符);
my_addr: 是一个指向sockaddr结构体类型的指针,不过由于系统兼容性的问题,一般不使用这个结构,而使用另外一个结构(struct sockaddr_in)来代替
addrlen:表示my_addr结构的长度,可以用sizeof操作符获得.
6、listen()
#include
int listen(int sockfd, int backlog)
功能:由主动监听模式变成被动监听模式,使内核能接受指向此套接字的连接请求,即服务端监听客户端发起的连接请求。
注:服务器端的listen() 函数:不是一个阻塞函数,将套接字和套接字对应队列的长度告诉Linux内核。
返回值:成功返回0,失败返回-1. 应用程序可通过WSAGetLastError()获取相应错误代码。
sockfd:由socket()调用返回的套接字描述符。
backlog:进入监听队列中允许的最大连接数目。 大多数系统的允许数目是20,我们可以设置为5到10
7、accept()
SOCKET accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:服务端接收客户端的连接请求。
返回值:成功,返回一个新的已连接的套接字描述符; 失败,返回-1
如果没有错误产生,则accept()返回一个描述所接受包的SOCKET类型的值。否则的话,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetLastError()来获得特定的错误代码。
参数:
sockfd:由socket()调用返回的套接字描述符,该套接口在listen()后监听连接。
addr:客户端的协议地址等信息
addrlen:(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数
注:若对客户端协议地址不感兴趣则可把addr和addrlen置为NULL
阻塞:已完成连接队列(completed connection queue),每个已完成 TCP 三次握手过程的客户对应其中一项。这些套接口处于 ESTABLISHED 状态。从处于 established 状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。
例:
accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);
8、connect()
#include
int connect(int sockfd , const struct sockaddr * servaddr, int addrlen);
功能:客户端向服务端申请连接,是一个阻塞函数,一般的情况下 客户端的connect函数 默认是阻塞行为直到三次握手阶段成功为止。通过TCp三次握手父服务器建立连接,通知Linux内核自动完成TCP 三次握手连接
返回值:成功则返回0, 失败返回-1, 错误原因存于errno 中。应用程序可通过WSAGetLastError()获取相应错误代码
sockfd:标识一个未连接socket,由shocket返回的套接口描述字
servaddr:指向要连接套接字的sockaddr结构体的指针
addrlen:sockaddr结构体的字节长度可设置成sizeof(struct sockaddr)
例:
int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
9、send()、recv()和sendto()、recvfrom()
(1)send()(可用write(代替))
int send(int sockfd,const void* msg,int len,int flags)
功能:发送指定数据到指定socket。
返回值:如果成功,则返回实际发送的数据;如果失败,则返回-1,错误原因存于errno。
参数:
(2)recv()(可用read()代替)
int recv(int sockfd,void* buf,int len,unsigned int flags)
功能:读取指定的socket的数据到指定缓冲区。
返回值:如果成功,则返回实际读取的数据;如果失败,则返回-1,错误原因存于errno。
参数:
(3)sendto()
int recv(int sockfd,void* buf,int len,unsigned int flags)
功能:向目标主机发送消息。
返回值:执行成功后返回实际发送数据的字节数,出错返回-1,错误代码存入errno中。
参数:
(4)recvfrom()
ssize_t recvfrom(int s,void *buf,size_t len,int flags,struct sockaddr *from,socklen_t *fromlen);
参数:
注:send()、sendto()和recv()、recvfrom()的区别
udp通讯中的sendto()需要在参数里指定接收方的地址/端口,recvfrom()则在参数中存放接收发送方的地址/端口,与之对应的send()和recv()则不需要如此,但是在调用send()之前,需要为套接字指定接收方的地址/端口(这样该函数才知道要把数据发往哪里),在调用recv()之前,可以为套接字指定发送方的地址/端口,这样该函数就只接收指定的发送方的数据,当然若不指定也可,该函数就可以接收任意的地址的数据。
udp服务器创建一个套接字接收客户端的连接,连接成功后,服务器再创建一个套接字与客户端进行数据交互,要求尽量使用connect()和recv()、send()函数。
10、close()
int close(int fd);
功能:用来关闭一个套接字描述符
返回值:执行成功返回0,出错则返回-1.错误代码存入errno中。
fd:一个套接字描述符
11、htons(), htonl(), ntohs(), ntohl()
(1)
h ---host 本地主机
to ---就是to 了
n ---net 网络的意思
l ---unsigned long
s ----short
(2)htons()
#include
uint16_t htons(uint16_t hostshort);
功能:将主机的无符号短整型数值转换为网络字字节顺序,即大端模式(big-endian)
返回值:TCP / IP网络字节顺序.
u_short hostshort: 16位无符号整数
(3)htonl()
#include
uint16_t htons(uint16_t hostshort);
功能:将主机的无符号长整形数转换成网络字节顺序
返回值:返回一个网络字节顺序的值
hostlong:主机字节顺序表达的32位数。
(4)ntohs()
#include
uint16_t ntohs(uint16_t netshort);
功能:将一个无符号短整型数从网络字节顺序转换为主机字节顺序
返回值:返回一个以主机字节顺序表达的数
netshort:一个以网络字节顺序表达的16位数
(5)ntonl()
Windows系统 :#include
linux系统 :#include
uint16_t ntohs(uint16_t netshort);
功能:将一个无符号长整形数从网络字节顺序转换为主机字节顺序
返回值:返回一个以主机字节顺序表达的数。
netlong:一个以网络字节顺序表达的32位数。
12、inet_addr()、inet_pton()、inet_ntop()、inet_ntoa()、inet_aton()
(1)inet_addr()
#include
in_addr_t inet_addr(const char *cp);
功能:将一个点分十进制的IP转换成一个长整数型数(u_long类型)。
返回:若字符串有效则将字符串转换为32位二进制网络字节序的IPV4地址,否则为INADDR_NONE
cp: 点分十进制的IP地址
(2)inet_pton()
windows下:
#include
linux下:
#include
#include
#include
int inet_pton(int af, const char *src, void *dst);
功能:将IP地址“点分十进制” 转换为“二进制整数”。
参数:
af=AF_INET
src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中。
af = AF_INET6
src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中。
返回值:如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。
(3)inet_ntop()
#include
#include
#include
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
功能:将IP地址“二进制整数”转换为“点分十进制”。
即:网络二进制结构转换为ASCII类型的地址。
返回值:成功返指向结果的指针,失败返NULL
参数:参数的作用和inet_pton相同。
不同之处socklen_t cnt,他是所指向缓存区dst的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC。
以上两个函数对ipv4和ipv6都适用。
(4)inet_ntoa()
#include< arpa/inet.h >
#include< Winsock2.h >
char* inet_ntoa(struct in_addr in);
功能:将一个十进制网络字节序转换为点分十进制IP格式的字符串。
返回值:如果正确,返回一个字符指针,指向一块存储着点分格式IP地址的静态缓冲区(同一线程内共享此内存);错误,返回NULL。
in:一个网络上的IP地址
(5)inet_aton()
#include
#include
#include
char* inet_ntoa(struct in_addr in);
功能:将一个点分十进制IP格式的字符串转换为十进制网络字节序。
即:将cp所指的C字符串转换为32位的网络字节序二进制序值。
返回值:成功返1,失败返0.
输入参数cp包含ASCII表示的IP地址。
输出参数inp是将要用新的IP地址更新的结构。