SOCKET网络编程 (通俗易懂入门篇)

时间:2022-07-23
本文章向大家介绍SOCKET网络编程 (通俗易懂入门篇),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

温故而知新☺

文章目录
  • ①预备知识
  • - IP地址转换函数
  • - sockaddr 数据结构

    • ②网络套接字函数
    • - socket函数
    • - bind 函数
    • - listen函数
    • - accept函数
    • - connect函数
    • - 补充5:
    • CS模型 - TCP
    • - Server
    • - Client
    • 写在最后

①预备知识

  • 在TCP/IP协议中,“IP地址+端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就称为端口号。

- IP地址转换函数

//旧版本就不看了,直接看新版本吧‘’

#include<arpa/inet.h>

int inet_pton(int af,const char *src,void *dst);  //[将“点分十进制” -> “整数”],这个函数转换字符串到网络地址.
//返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);
//返回值:若成功则为指向结构的指针,若出错则为NULL

//支持ipv4和ipv6
//可重入函数

//参数释义
// af:参数既可以是AF_INET也可以是AF_INET6。
// src:第一个函数尝试转换由src指针所指向的字符串,并通过dst指针存放二进制结果
// inet_ntop进行相反的转换,从数值格式(addrptr)转换到表达式(strptr)。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。为有助于指定这个大小,在

#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

//如果len太小,不足以容纳表达式结果,那么返回一个空指针,并置为errno

- sockaddr 数据结构

struct sockaddr :很多网络编程函数的出现早于IPV4协议,为了向前兼容,现在sockaddr都退化成(void *)结构了。 传递一个地址给函数,然后由函数内部再强制类型转换为所需的地址类型。

struct sockaddr
 {
	unsigned short sa_family; /* address族, AF_xxx */
	char sa_data[14]; 	  /* 14 bytes的协议地址 */
};

//   sa_family 一般来说, IPV4使用“AF_INET”。
struct sockaddr_in 
{
	short int sin_family; /* Internet地址族 */
	unsigned short int sin_port; /* 端口号 */
	struct in_addr sin_addr; /* Internet地址 */
	unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小)*/
};

//这两个数据类型是等效的,可以相互转换,通常使用sockaddr_in更为方便

补充1: sockaddr和sockaddr_in的抉择: 前者是通用的套接字结构体,它可以在不同的协议族之间进行强转。 后者是以太网中采用的套接字结构体,因为前面那个不好用。

由于两个结构体的大小一致,所以进行地址结构设置的时候,通常的方法是使用后者进行配置,然后强制转换为前者的结构体类型,这样不会有任何副作用。

②网络套接字函数

基于流套接字的网络编程流程:

//头文件

#include<sys/type.h>
#include<sys/socket.h>

- socket函数

//socket函数:

int socket(int domain,int type,int protocol);

//参数释义:
domain:
AF_INET:用来产生IPV4 - socket 的协议,使用TCP或UDP来传输,用IPV4的地址
AF_INET6:和上面的差不多,这个是IPV6的
AF_UNIX:本地协议,用在Unix和Linux系统上,一般都是服务端和客户端在同一台机器上时使用。

type:
SOCK_STREAM:这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,是用TCP协议来传输的。
SOCK_DGRAM:这个协议是无连接的,固定长度的连接调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET:这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。(注(1))必须把整个包完整的接收才能够进行读取。
SOCK_RAW:这个socket类型提供单一的网络访问

protocol:0,默认协议

返回值:
	成功返回一个文件描述符,失败返回-1,设置errno。

补充2: 以太网中建议使用PF_INET这个域。在程序设计时会发现有的代码使用的是AF_INET,在头文件中这两个值是一致的,我也经常写AF_INET,不过还是有细微的差别,PF_INET有些协议类型是AF_INET所没有实现的。

socket函数打开一个网络通讯端口,如果成功的话就像open一样返回一个文件描述符,应用程序可以像读写文件一样read/write在网络上收发数据。

- bind 函数

int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

参数释义:
sockfd:socket文件描述符
addr:构造出IP地址加端口号
addrlen:sizeof(addr)的长度 

客户端程序得之服务器程序的地址和端口号后就可以自动向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。

bind函数的作用就是将参数sockfd和addr绑定在一起,使sock奋斗这个用于网络通信的描述符监听addr所描述的地址和端口号。 至于addr的配置可以参考前面的sockaddr自己配置,也可以看后面栗子里面的配置。 因为addr的配置不确定,所以需要第三个参数来指定结构体长度。

栗子:

struct sockaddr_in servaddr;
bzero(&servaddr,sizeof(servaddr));  //清空servaddr内容
servaddr.sin_family = PF_INET;
servaddr.sin_addr = htonl(INADDR_ANY);  //这个宏表示任意的IP地址
//服务器一般有多个网卡,每个网卡也可能绑定了多个IP地址,这样可以设置在所有IP地址上监听,直到与某个客户端建立连接。
servaddr.sin_port = htons(8000);

- listen函数

int listen(int sockfd,int backlog);

参数释义:
	backlog:排队建立三次握手队列和刚建立三次握手队列的链接数和

cat /proc/sys/net/ipv4/tcp_max_syn_backlog  查看系统默认backlog

listen函数声明sockfd处于监听状态,并且最多允许backlog个客户端处于连接状态,如果多了就忽略。

成功返回0,失败返回-1.

补充3: 函数listen用来初始化服务器可连接队列。 服务器处理客户端连接时是顺序处理的,同一时间只能处理一个客户端连接。 当多个客户端的连接请求同时到来的时候,服务器将不能处理的客户端连接请求放入到等待队列中,这个队列的长度由listen()函数来指定。

大多数系统的设置为20,其实真的没必要太多,真的。 根据系统的可承受负载和程序的需求来确定。 系统有一个最大侦听队列数,一般是128(somaxconn),可以调优。

- accept函数

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

参数释义:
	addr:传出参数,返回连接客户地址信息,含IP地址和端口号。
	addrlen:传入addr的大小,返回真正的大小。

返回值:成功返回一个新的sockfd,用于和客户端通信,失败返回-1.

三方握手完成后,服务器调用accept接收连接,如果服务器调用accept时还没有客户端请求连接,就阻塞等待直到有客户端连接上来。 如果addr传NULL,则表示不关心客户端的地址。

while(1)
{
	cliaddr_len = sizeof(cliaddr);
	connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);  //那个强转,历史遗留问题
	n = read(connfd,buf,MAXLINE);
	···
	close(connfd);
}

- connect函数

int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

参数释义:
addr:传入参数,指定服务器的地址信息,含IP地址和端口号。

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

客户端需要调用connect连接服务器 connect和bind形式一致,区别在于connect是用对方的地址。

- 补充5:

关闭套接字函数不止一个close,还有shutdown。

int shutdown(int sock,int how); //该函数用于关闭双向连接的一部分。 /* how: SHUT_RD:值为0,表示切断读 SHUT_WR:值为1,表示切断写 SHUT_RDWR:值为2,和close功能相同 */

CS模型 - TCP

- Server

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/type.h>
#include<netinet/in.h>
#include<arpa/inet.h>

#define MAXLINE 80
#define SERV_PORT 8000

int main(void)
{
	struct saockaddr_in servaddr,cliaddr;
	socklen_t cliaddr_len;
	int listenfd,connfd;

	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int i,n;

// 套路开始
	listenfd = socket(PF_INET,SOCK_STREAM,0);	//创建一个网络套接字
	
	bzero(&servaddr,sizeof(servaddr));	//清空结构体变量,准备开始刻画
	servaddr.sin_family = PF_INET;  //配置网络协议
	servaddr.sin_port = htonl(SERV_PORT);  //配置端口号
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  //配置网络地址
	bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));  //好了,可以绑定了  别忘了历史遗留问题

	listen(listenfd,20);  //开始监听,允许20个进程进来


//开始接收数据了
	printf("Accepting connections···  n");  //写完一定要来检查一下这个换行,一不小心就忘记了
	while(1)
	{
		cliaddr_len = sizeof(cliaddr);  //这得实时更新
		connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);  //接收连接
		
		n = read(connfd,buf,MAXLINE);  //处理事务(这里为读取内容)
		printf("Read from %s at port %d n",inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));
		/*将客户端的地址读取到str里面然后打印*/  /*将端口号转换成整形数输出*/
		
		for(i = 0;i < n; i++)
		{
			buf[i] = toupper(buf[i]);  //换大写
		}	
		write(connfd,buf,n);  //写回去
		close(connfd);//用完关咯
	}

	//这里就不用return 0 了
}

- Client

/*client.c*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/type.h>
#include<netinet/in.h>

#define MAXLINE 80
#define SERV_PORT 8000

int main(int argc,char *argv[])
{
	struct sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd,n;
	char *str;
	
	if(argc!=2)  //(注(2))
	{
		fputs("usage: ./c;ient message n",stderr);
		exit(1);
	}
	str = argv[1];
	
	sockfd = socket(PF_INET,SOCK_STREAM,0);
	
	bzero(&servaddr,sizeof(servaddr));	//清空结构体变量,准备开始刻画
	
	servaddr.sin_family = PF_INET;
	inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT); 
	
	bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
	
	connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));//请求连接服务器

	write(sockfd,str,strlen(str));  //写入

	n = read(sockfd,buf,MAXLINE);
	printf("Response from server : n···");
	write(STDOUT_FIFENO,buf,n);  //写入标准输出流

	close(sockfd);
	return 0;	
}

写在最后

今天回头做网络方面的一个项目,手有点生。 想起一个朋友说:

一天不练,自己知道; 两天不练,同行知道; 三天不练,观众知道。

网络编程这块可不能丢。