网络编程示例之socket编程
理解socket
套接字(socket)属于糟糕的翻译,因为它太生僻。“套接”生活中用得少,加个“字”,成了套接字。当你学了socket以后,你还是很难将名字与它的意思联系起来。香港翻译成:網路插座 感觉贴近一些
socket编程流程
socket编程就是套路,很多代码都是可重用的。而且步骤很固定,下面给出一个流程(事实上网络编程就是按照这个流程来的,希望大家能记住这个)。
函数顺序
大家只要牢记这一流程去操作,那么进行简单的单道程序设计是很简单的,也就是我们常说的一对一的服务器和客户端得关系。如果你想要创建一个服务器同时连接多个客户端的话就要去了解 多进程或者多线程了。但是这个效率并不是很高,这样一直开下去负担是很大的,所以就会有高并发。大家可以先学习函数顺序,多进程、高并发等概念等将来深入之后再回过来了解。
套接字函数
(一)创建套接字-socket()
应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段。
其调用格式如下:
SOCKET PASCAL FAR socket(int af, int type, int protocol);
该调用要接收三个参数:af、type、protocol。参数af指定通信发生的区域:AF_UNIX、AF_INET、AF_NS等,而DOS、WINDOWS中仅支持AF_INET,它是网际网区域。因此,地址族与协议族相同。参数type 描述要建立的套接字的类型。
这里分三种:
(1)一是TCP流式套接字(SOCK_STREAM)提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)即使用流式套接字。
(2)二是数据报式套接字(SOCK_DGRAM)提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。
(3)三是原始式套接字(SOCK_RAW)该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的“协议”这一元。
(二)指定本地地址-bind()
当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。
其调用格式如下:
int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen);
参数s是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。参数name 是赋给套接字s的本地地址(名字),其长度可变,结构随通信域的不同而不同。namelen表明了name的长度。如果没有错误发生,bind()返回0。否则返回SOCKET_ERROR。
(三)建立套接字连接-connect()与accept()
这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接。accept()用于使服务器等待来自某客户进程的实际连接。
(1)connect()的调用格式如下:
int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
参数s是欲建立连接的本地套接字描述符。参数name指出说明对方套接字地址结构的指针。对方套接字地址长度由namelen说明。如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。由于地址族总被包含在套接字地址结构的前两个字节中,并通过socket()调用与某个协议族相关。因此bind()和connect()无须协议作为参数。
(2)accept()的调用格式如下:
SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
参数s为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。
accept()用于面向连接服务器。参数addr和addrlen存放客户方的地址信息。调用前,参数addr 指向一个初始值为空的地址结构,而addrlen 的初始值为0;调用accept()后,服务器等待从编号为s的套接字上接受客户连接请求,而连接请求是由客户方的connect()调用发出的。当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr 和addrlen,并创建一个与s有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。
四个套接字系统调用,socket()、bind()、connect()、accept(),可以完成一个完全五元相关的建立。socket()指定五元组中的协议元,它的用法与是否为客户或服务器、是否面向连接无关。bind()指定五元组中的本地二元,即本地主机地址和端口号,其用法与是否面向连接有关:在服务器方,无论是否面向连接,均要调用bind(),若采用面向连接,则可以不调用bind(),而通过connect()自动完成。若采用无连接,客户方必须使用bind()以获得一个唯一的地址。
(四)监听连接-listen()
此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用。
其调用格式如下:
int PASCAL FAR listen(SOCKET s, int backlog);
参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。
listen()在执行调用过程中可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。
调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。
(五)数据传输-send()与recv()
当一个连接建立以后,就可以传输数据了。常用的系统调用有send()和recv()。
(1)send()调用用于s指定的已连接的数据报或流套接字上发送输出数据。
格式如下:
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
参数s为已连接的本地套接字描述符。buf 指向存有发送数据的缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否发送带外数据等。如果没有错误发生,send()返回总共发送的字节数。否则它返回SOCKET_ERROR。
(2)recv()调用用于s指定的已连接的数据报或流套接字上接收输入数据,格式如下:
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);
参数s 为已连接的套接字描述符。buf指向接收输入数据缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否接收带外数据等。如果没有错误发生,recv()返回总共接收的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR。
(六)关闭套接字-close()
close()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。
简单代码实现TCP协议的服务器
这段代码实现了一个简单的单线程TCP服务器,它可以接受客户端连接,并发送响应消息。当客户端连接时,服务器会接收客户端发送的消息,并向客户端发送"Hello, Client!"的响应消息。这是一个单线程服务器,只能处理一个客户端连接。
现在我们来逐步解释代码:
(一)包含了一些必要的头文件,包括stdio.h、stdlib.h、string.h、sys/socket.h、arpa/inet.h和unistd.h。这些头文件提供了与套接字、网络通信等相关的函数和数据结构。
(二)定义了一些常量,包括服务器的IP地址(SERVER_IP)、端口号(SERVER_PORT)以及接收数据的缓冲区大小(BUFFER_SIZE)。
(三)主函数main()开始执行,定义了一些变量:server_fd表示服务器套接字,clientSocket表示客户端套接字,serveraddr和clientaddr分别表示服务器和客户端的地址信息,clientaddrlen表示客户端地址信息的长度,buffer表示用于存储数据的缓冲区。
(四)创建客户端套接字:调用socket函数创建一个服务器端套接字,并检查返回值是否为-1。如果创建失败,程序打印出错信息,并使用exit(1)终止程序。
(五)设置服务器地址信息:将服务器地址结构体serveraddr的各个字段清零,然后设置地址族为IPv4(AF_INET),端口号为SERVER_PORT,IP地址为INADDR_ANY,表示服务器可以监听任意网络接口上的连接。
(六)绑定套接字到指定地址和端口:调用bind函数将套接字绑定到指定的地址和端口,如果绑定失败,程序打印出错信息,并使用exit(1)终止程序。
(七)监听连接请求:调用listen函数开始监听连接请求,如果监听失败,程序打印出错信息,并使用exit(1)终止程序。服务器可以接受的等待连接的最大数量为5。
(八)打印服务器监听的端口号,表示服务器已经在该端口号上进行监听。
(九)接受客户端连接:调用accept函数接受客户端的连接请求。如果接受失败,程序打印出错信息,并使用exit(1)终止程序。
(十)打印客户端的IP地址,表示客户端已成功连接。
(十一)接收数据:使用recv函数接收客户端发送的数据,并将数据存储在buffer缓冲区中。如果接收失败,程序打印出错信息,并使用exit(1)终止程序。
(十二)打印接收到的客户端消息。
(十三)发送响应:使用send函数向客户端发送响应消息,即"Hello, Client!"。如果发送失败,程序打印出错信息,并使用exit(1)终止程序。
(十四)关闭套接字:调用close函数关闭客户端套接字和服务器套接字。
(十五)返回0,表示程序顺利执行结束。