socket编程(一):TCP/UDP基础篇

这篇博客介绍了TCP/UDP的基础知识,包括关键的socket地址结构体、如何建立连接和通讯,以及TCP和UDP的通信流程。文章详细阐述了socket编程中结构体的初始化、端口的重要性、创建套接字、绑定端口/IP、TCP与UDP的通信过程,以及简单的TCP和UDP代码实现。此外,还提及了进阶的socket选项设置和获取套接字地址的函数。
摘要由CSDN通过智能技术生成


关于 socket的编程,有很多知识。本文讲述一下这部分比较基础的点。

1. 关键的结构体

在说sockaddr_in之前,我觉得有必要列出来sockaddr这个结构体。

1.1. sockaddr

sockaddr是通用的socket地址,其定义在#include <sys/socket.h>中。

struct sockaddr
{
   
	sa_family_t		sin_family;	//地址族,一般使用AF_INET
	char			sa_data[14]; //14 bytes 协议地址
};

此结构体用作bind,connect, recvfrom, sendto等函数的参数中,以指明地址信息。但一般编程并不会使用它,而是使用等价的结构体sockaddr_in

1.2. sockaddr_in

看过一点相关代码的朋友,一定对这组结构体不陌生:

struct sockaddr_in
{
   
	sa_family_t		sin_family;		//地址族,一般使用AF_INET
	unit16_t		sin_port;		//16位TCP/UDP端口号
	struct in_addr	sin_addr;		//32位IP地址
	char			sin_zero[8];	//一般不使用
};

而其中包含的结构体定义为:

struct in_addr
{
   
	In_addr_t	s_addr;		//32位IPv4地址
};

sockaddr_in在头文件#include<netinet/in.h>#include <arpa/inet.h>中定义,该结构体把portaddr 分开储存在两个变量中,这种方式解决了sockaddr的缺陷。

1.3. 说明

  • sin_family是地址族(Address Family),其格式位AF_XXXX,对于socket编程,全部使用AF_INET,这代表了TCP\UDP
  • sin_portsin_addr(s_addr)都必须是网络字节序(NBO),一般可视化的数字都是主机字节序(HBO)。
  • sin_zero虽然不怎么使用,但也要明白是干嘛的。其作用是为了使sockaddrsockaddr_in两个数据结构大小保持相同而保留的空字节。
  • sockaddr_insockaddr是并列的结构,指向sockaddr_in的结构体的指针也可以指向sockaddr的结构体,并代替它。也就是说,你可以使用sockaddr_in建立你所需要的信息, 然后用进行类型转换就可以了。

1.4. 初始化结构体

	#define MY_PORT 8888
    int mySocket;                        /*服务器套接字文件描述符*/
    struct sockaddr_in mySocket_addr;
    /*初始化地址*/
    bzero(&mySocket_addr, sizeof(struct sockaddr_in));      /*清零*/
    mySocket_addr.sin_family = AF_INET;                		/*AF_INET协议族*/
    mySocket_addr.sin_addr.s_addr = htonl(INADDR_ANY); 		/*任意本地地址*/
    mySocket_addr.sin_port = htons(MY_PORT);       			/*定义的端口*/

1.5. 初始化的注释

1.5.1. 端口号

端口是很重要的一个概念,我这里端口号8888只是我随意写的一个例子。端口是一种抽象的结构,包括数据结构和I/O缓冲区。
应用程序(即进程)通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在TCP/IP协议的实现中,端口操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问之。
类似于文件描述符,每个端口都拥有一个叫端口号(port number)的整数型标识符,用于区别不同端口。

这是我找到的一个讲的很好的博文:TCP/UDP共用端口问题

1.5.2. htons(), htonl(), ntohs(), ntohl()函数说明

初始化的时候,用到了这两个函数htonshtonl,那么本节就说下这两个以及相关的函数。

函数 说明
uint16_t htons(uint16_t netshort) 此函数将将主机的无符号短整形数转换成网络字节
顺序字节顺序 ,返回值为一个网络字节顺序的值
uint32_t htonl(uint32_t hostlong) 将一个32位数从主机字节顺序转换成网络字节顺序,
返回一个网络字节顺序的值
uint16_t ntohs(uint16_t netshort) 将一个16位数由网络字节顺序转换为主机字节顺序,
返回一个以主机字节顺序表达的数
uint32_t ntohl(uint32_t netlong) 将一个32位数从网络字节顺序转换为主机字,
返回一个以主机字节顺序表达的数

我在进行初始化,自然要转地址位为网络地址,所以使用htonshtonl。而正如上面所说,sin_port是16位,s_addr是32位,因而如此分别使用:

    mySocket_addr.sin_addr.s_addr = htonl(INADDR_ANY); 		/*任意本地地址*/
    mySocket_addr.sin_port = htons(MY_PORT);       			/*定义的端口*/

在此需要说明,INADDR_ANY是指地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。当初始化,不确定地址时候,可以使用。如此将开启计算机所有IP地址/网卡等待接收信号。
打个比方,比如你的机器有三个ip:192.168.1.1 202.202.202.202 61.1.2.3。如果你预设的值为server.sin_addr.s_addr=inet_addr("192.168.1.1");,然后监听100端口。这时其他机器只有连接 192.168.1.1:100才能成功。 连接202.202.202.202:10061.1.2.3:100都会失败。 但是如果server.sin_addr.s_addr=htonl(INADDR_ANY); 的话,无论连接哪个ip都可以连上的。

1.5.3. inet_ntoa()inet_addr()

说到地址转化函数,这两个也需要提一下。

char *inet_ntoa(struct in_addr in)  
将一个32位网络字节序的二进制IP地址转换成相应的点分十进制的IP地址
in_addr_t inet_addr(const char *cp)
参数为一个点分十进制的IP地址,如果正确执行将返回一个无符号长整数型数。
如果传入的字符串不是一个合法的IP地址,将返回INADDR_NONE。






2. 建立连接并且通讯

这一节将说一下建立连接和通讯的相关函数。

2.1. 创建套接字: socket()

2.1.1. 函数说明

int socket( int af, int type, int protocol)
  • af:一个地址描述。仅支持AF_INET格式,即上面所说的mySocket_addr.sin_family = AF_INET
  • type:指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等。
  • protocol:顾名思义,就是指定协议。套接口所用的协议。如调用者不想指定,可用0(一般都用0)。常用的协议有,IPPROTO_TCPIPPROTO_UDPIPPROTO_STCPIPPROTO_TIPC等,它们分别对应TCP传输协议UDP传输协议STCP传输协议TIPC传输协议

2.1.2. 举例

TCPSocket = socket(AF_INET, SOCK_STREAM, 0)  	//TCP面向字节流(SOCK_STREAM)
UDPSocket = socket(AF_INET, SOCK_DGRAM, 0)		//UDP面向报文(SOCK_DGRAM)

2.1.3. 适用范围

UDP/TCP 的 服务端/客户端 均需要

2.2. 绑定端口/IP: bind()

2.2.1. 函数说明

int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
  • sockfd是用socket()函数创建的文件描述符。
  • my_addr是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要弦将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等接合在一起。
  • addrlenmy_addr结构的长度,可以设置成sizeof(struct sockaddr)。使用sizeof(struct sockaddr)来设置套接字的类型和其对已ing的结构。
  • 返回值为0时表示绑定成功,-1表示绑定失败,errno的错误值请自行百度。

2.2.2. 举例

if(bind(serverSocket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
   
	perror("bind error");
	return 1;
}

2.2.3. 适用范围

适用于TCP\UDP的服务端
这里解释下为何只用于服务端:
首先说明原理,bind函数就是绑定, 将一个socket绑定到一个地址上, 也可以这么说:bind函数对一个socket进行命名(注意:socket名称包括三要素: 协议, ip, port)

  • 对于TCP的服务端bind()函数是一定需要的。可以做个小实验试下,如果不使用bind(),那么客户端在进行连接时候就会报错Connection refused。这很好理解,TCP是面向连接的,服务器是时时在监听(listen)有没有客户端的连接。如果服务器不绑定IP和端口的话(bind),客户端上线的时候怎么连到服务器呢。所以服务器要绑定IP和端口,而客户端就不需要了。

  • 对于TCP的客户端bind()一般是不需要的。客户端上线是主动向服务器发出请求的,因为服务器已经绑定了IP和端口,所以客户端上线的就向这个IP和端口发出请求,这时因为客户开始发数据了(发上线请求),系统就给客户端分配一个随机端口,这个端口和客户端的IP会随着上线请求一起发给服务器, 服务器收到上线请求后就可以从中获起发此请求的客户的IP和端口,接下来服务器就可以利用获起的IP和端口给客户端回应消息了。总之一句话:客户端是主动连接(connect), 而服务器是等待接受连接(accept)。

  • 对于UD

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值