TCP、UDP、网络编程

1、TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下:

(1)TCP服务端编程的一般步骤为:

  1. 调用socket函数,创建一个套接字描述符。
  2. 创建网络地址结构,指定要监听的IP和端口号。
  3. 调用bind函数,将套接字描述符与网络地址结构绑定。
  4. 调用listen函数,将套接字描述符转为监听套接字,表示该描述符是用于从指定地址和端口接收连接的。
  5. 调用accept函数来获取链接。
  6. 得到连接后使用readwrite函数完描述符里读写数据。
  7. 完成后调用close关闭描述符。

2TCP客户端端编程的一般步骤为:

  1. 调用socket函数,创建一个套接字描述符。
  2. 创建网络地址结构,指定要连接的服务端的IP和端口号。
  3. 调用connect函数连接服务端。
  4. 连接成功后调用readwrite函数读写数据
  5. 完成后调用close关闭描述符。

注:

(1)套接字

TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字(socket)或插口。

套接字用(IP地址:端口号)表示。

它是网络通信过程中端点的抽象表示,包含进行网络通信必需的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

(2)套接口

通讯的基石是套接口,一个套接口是通讯的一端。在这一端上你可以找到与其对应的一个名字。一个正在被使用的套接口都有它的类型和与其相关的进程。套接口存在于通讯域中。通讯域是通过套接口通讯来处理一般的线程而引进的一种抽象概念。

(3)错误处理:包裹函数

包裹函数调用实际函数,检查返回值,发生错误终止进程。确定包裹函数名的约定是大写实际函数名的第一个字符。

(4)三次握手

a、对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。

         通常的情况,客户端的 connect() 函数默认会一直阻塞,直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。

blisten() 函数的主要作用就是将套接字( 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 <arpa/inet.h>

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 <sys/socket.h>中定义,sockaddr的缺陷是sa_data把目标地址和端口混在一起,如下

sin_family:表示地址类型,对于使用TCP/IP协议进行的网络编程,该值只能是AF_INET

sa_data:存储具体的协议地址

(3)sockaddr_in(最常用)

sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:

  • sin_family:表示地址类型,对于使用TCP/IP协议进行的网络编程,该值只能是AF_INET.
  • sin_port:是端口号
  • sin_addr:用来存储32位的IP地址
  • 数组sin_zero为填充字段,一般赋值为0.

(4)bzero
#include<string.h>

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 <sys/socket.h>

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等等。

  • SOCK_STREAM:创建TCP流套接字
  • SOCK_DGRAM:创建UDP数据报套接字
  • SOCK_RAW:创建原始套接字
  • protocol:套接口所用的协议。如调用者不想指定,可用0指定,表示通过参数af指定的协议族和参数type指定的套接字类型来确定使用的协议。当为原始套接字时,系统无法唯一的确定协议,此时就需要使用该参数指定所使用的协议。

常用的协议有,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 <winsock.h>

int PASCAL FAR bind( SOCKET sockaddr, const struct sockaddr FAR* my_addr,int addrlen);

(2)在Linux环境为:

#include <sys/types.h>

#include <sys/socket.h>

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<sys/socket.h>

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 <sys/socket.h>

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。

参数:

  •    sockfd:希望给发送数据的套接字描述符 。
  •    msg:    希望发送的数据的缓冲区。
  •    len:      希望发送的数据的长度。     
  •  flags:      发送模式,一般设置为0。

(2)recv()(可用read()代替)

int recv(int sockfd,void* buf,int len,unsigned int flags)

功能:读取指定的socket的数据到指定缓冲区。

返回值:如果成功,则返回实际读取的数据;如果失败,则返回-1,错误原因存于errno。

参数:

  •    sockfd:希望读取数据的套接字描述符 。
  •    buf:      读取的数据所存放的缓冲区。
  •    len:      允许读取的数据的最大长度。
  •  flags:      读取模式,一般设置为0。

(3)sendto()

int recv(int sockfd,void* buf,int len,unsigned int flags)

功能:向目标主机发送消息。

返回值:执行成功后返回实际发送数据的字节数,出错返回-1,错误代码存入errno中。

参数:

  • s:套接字描述符。
  • msg:发送缓冲区
  • len:待发送数据的长度
  • flags:控制选项,一般设置为0或取下面的值
    • (1)MSG_OOB:在指定的套接字上发送带外数据(out-of-band data),该类型的套接字必须支持带外数据(eg:SOCK_STREAM).
    • (2)MSG_DONTROUTE:通过最直接的路径发送数据,而忽略下层协议的路由设置。
  • to:用于指定目的地址
  • tolen:目的地址的长度。

(4)recvfrom()

ssize_t recvfrom(int s,void *buf,size_t len,int flags,struct sockaddr *from,socklen_t *fromlen);
  • 功能:接收数据。
  • 返回值:执行成功后返回实际接收到数据的字节数,出错时则返回-1,错误代码存入errno中。

参数:

  • s:套接字描述符
  • buf:指向接收缓冲区,接收到的数据将放在这个指针所指向的内存空间。
  • len:指定了缓冲区的大小。
  • flags:控制选项,一般设置为0或取以下值
    • (1)MSG_OOB:请求接收带外数据
    • (2)MSG_PEEK:只查看数据而不读出
    • (3)MSG_WAITALL:只在接收缓冲区时才返回。
  • from:保存了接收数据报的源地址。
  • fromlen:参数fromlen在调用recvfrom前为参数from的长度,调用recvfrom后将保存from的实际大小。

注: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 <arpa/inet.h> 

uint16_t htons(uint16_t hostshort); 

功能:将主机的无符号短整型数值转换为网络字字节顺序,即大端模式(big-endian)

返回值:TCP / IP网络字节顺序.

u_short hostshort: 16位无符号整数

(3)htonl()

#include <arpa/inet.h>  

uint16_t htons(uint16_t hostshort); 

功能:将主机的无符号长整形数转换成网络字节顺序

返回值:返回一个网络字节顺序的值
hostlong:主机字节顺序表达的32位数。

(4)ntohs()

#include <arpa/inet.h>

uint16_t ntohs(uint16_t netshort);

功能:将一个无符号短整型数从网络字节顺序转换为主机字节顺序

返回值:返回一个以主机字节顺序表达的数

netshort:一个以网络字节顺序表达的16位数

(5)ntonl()

Windows系统 :#include<Winsock2.h>

linux系统 :#include <arpa/inet.h>

uint16_t ntohs(uint16_t netshort);

功能:将一个无符号长整形数从网络字节顺序转换为主机字节顺序

返回值:返回一个以主机字节顺序表达的数。

netlong:一个以网络字节顺序表达的32位数。

12、inet_addr()、inet_pton()、inet_ntop()、inet_ntoa()、inet_aton()

(1)inet_addr()

#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);

功能:将一个点分十进制的IP转换成一个长整数型数(u_long类型)。

返回:若字符串有效则将字符串转换为32位二进制网络字节序的IPV4地址,否则为INADDR_NONE

cp: 点分十进制的IP地址

(2)inet_pton()

windows下:

#include <WS2tcpip.h>

linux下:

#include <sys/socket.h>

#include <netinet/in.h>

#include<arpa/inet.h>

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 <sys/types.h>

#include <sys/socket.h>

#include <arpa/inet.h>

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地址

5inet_aton()

#include <sys/types.h>

#include <sys/socket.h>

#include <arpa/inet.h>

char* inet_ntoa(struct in_addr in);

功能:将一个点分十进制IP格式的字符串转换为十进制网络字节序。

即:将cp所指的C字符串转换为32位的网络字节序二进制序值。

返回值:成功返1,失败返0.

输入参数cp包含ASCII表示的IP地址。

输出参数inp是将要用新的IP地址更新的结构。

猜你喜欢

转载自blog.csdn.net/King_weng/article/details/83422306
今日推荐