服务器编程的基本步骤

一、服务器端

   作为服务器端,要想使客户端能够正常访问,必须有自己的地址,使客户端能够在网络中找到该服务器,就相当于在茫茫人海中想和某人交流,必须知道她在哪儿。与此同时,两个人找到了语言不通也无法进行交流,服务器和客户端的进程亦是如此,不使用相同的协议也无法进行通信,因此服务器端需指定所使用的协议。当两个人相遇之后,还有必不可少的一步就是确认身份,在服务器和客户端通信过程中,客户端在网络中找到了服务器端,必须得到服务器端的接受(建立连接)才可进行通信。这一系列操作完成之后两个人方可你一言我一语(接收信息、发送信息)愉快的交流,最后交流结束两人互相告别,即为释放连接的操作。

    在客户端寻址服务器的时候还可以进一步进行划分,客户端可以通过ip地址在网络中找到服务器主机的位置,但是在一台主机上可能运行着很多进程,而客户端需要通信的可能只是其中的某一个进程或应用程序,因此还需进一步寻址,找到要建立通信的进程,这就需要知道该进程的端口号,客户端可以通过端口号找到具体的进程。

具体流程如下:

  1. socket() 创建套接字:指定底层协议族;指定服务类型。
  2. bind() 命名socket:指定服务器ip地址并告诉计算机我要使用某个端口。
  3. listen() 监听socket:建立一个监听序列以存放待处理的客户连接。
  4. accept() 处理连接,从监听队列中接受一个连接。
  5. recv() send() 客户端与服务器进行通信。
  6. close() 通信完成,断开连接。 

二、客户端

  客户端访问服务器,也必须按照服务器的协议进行操作,因此客户端需指定本身所使用的底层协议和服务器相同(即上例中的语言相通)。接下来就是客户端在网络中找服务器,给定客户端服务器的ip地址和具体进程或应用程序的端口号,客户端即可找到服务器。客户端找到服务器之后就申请建立连接,等待服务器接受连接之后,客户端便可与服务器进行通信。最后通信结束关闭连接。

具体流程如下:

  1. socket() 创建socket,功能和服务器相同
  2. connect() 申请连接:需指定服务器ip地址和相关进程的端口号
  3. send() recv( ) 进行通信
  4. close() 关闭连接

三、步骤详解

1、创建套接字

      为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数,指定期望的通信协议类型。在unix/linux操作系统中,一切皆文件,socket也不例外它就是可读可写而控制可关闭的文件描述符。以下是创建socket的系统调用。

# include<sys/types.h>
# include<sys/stat.h>
int socket(int domain, int type, int protocol);

domain参数:指定底层协议族。
            tcp/ip:PF_INET(ipv4)  PF_INET6(ipv6)
            unix本地域协议族:PF_UNIX
type参数:指定服务类型
 
         SOCK_STREAM:流式服务(遵守tcp服务)
         SOCK_UGRAM:数据报服务(遵守udp服务)
protocol:恒置0

2、命名socket

  创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。将一个socket与socket地址绑定称为给socket命名。服务器端只有命名后客户端才能知道该如何连接它,客户端通常不需要命名socket,而是采用匿名的方式,即使用操作系统自动分配的socket。命名socket用bind()系统调用。

# include<sys/types.h>
# include<sys/stat.h>

int bind(int sockfd, const struct struct sockaddr *myaddr, socklen_t addrlen);
bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出socket地址长度,成功返回0,失败返回-1;
socket地址分为通用socket地址和专用socket地址:

通用socket地址:
# include<bits/socket.h>
struct sockaddr
{
    sa_family_t;    //地址族类型(与协议族一一对应)
    char sa_data[14];    //存放socket地址值
}
专用socke地址:
1、UNIX本地域协议族专用socket地址
# include<sys/un.h>
struct sockaddr_un
{
    sa_family_t sin_family;    //地址族,AF_UNIX
    char sun_path[108];    //    文件路径名
}

2、tcp/ip协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,他们分别用于ipv4和ipv6
struct sockaddr_in
{
    sa_family sin_family;    //地址族,AF_INET
    u_int16_t sin_port;    //端口号,要用网络字节序表示
    struct in_addr sin_addr;    //ipv4地址结构体
}

struct in_addr
{
    u_int32_t s_addr;    //ipv4地址,要用网络字节序表示
}


struct sockaddr_in6
{
    sa_family_t sin6_family;    //地址族,AF_INET6
    u_int16_t sin6_port;    //端口号,要用网络字节序表示
    u_int32_t sin6_flowinfo;    //流信息,应设置为0
    struct int6_addr sin6_addr;    //ipv6地址结构体
    u_int32_t sin6_scope_id;
}

struct in6_addr
{
    unsigned char sa_addr[16];    //ipv6地址,要用网络字节序表示
}
协议族和地址族的关系
协议族地址族描述
PF_UNIXAF_UNIXUNIX本地域协议族
PF_INETAF_INETtcp/ipv4协议族
PF_INET6AF_INET6tcp/ipv6协议族

3、监听socket

socket被命名之后,还不能马上接受客户连接,我们需要使用系统调listen用来创建一个监听队列以存放待处理的客户连接,listen系统调用有做两件事:

  • 当函数socket创建一个套接口时,它假设为一个主动套接口,也就是说,它是一个将调用connect发起连接的客户套接口,函数listen将未连接的套接口转换成被动套接口,指示内核应接受指向套接口的连接请求。调用函数listen导致套接口从CLOSED状态转换到LISTEN状态
  • 函数第二个参数在linux和unix系统中有所差异,具体如下:

      为了理解参数backlog,我们必须明白,对于给定的监听套接口,内核需要维护两个队列:(1)未完成连接队列,为每个这样的SYN分节开设一个条目:已由客户发出并到达服务器,服务器正在等待完成相应的TCP三次握手过程。这些套接口都处于SYN_RCV状态。(2)已完成连接队列,为每个已完成TCP三次握手过程的客户开设一个条目。这些套接口都处于ESTABLISHED状态。

      在linux系统中,backlog参数是以上两个队列和的最大值,在unix系统中,backlog参数是已完成三次握手的队列(也就是上边提到的第二个队列)的最大值。

# include<sys/socket.h>
int listen(int sockfd, int backlog);
参数sockfd:指被监听的socket

4、接受连接

接受连接是指从listen监听队列中接受一个连接,若已完成三次握手的队列为空,则进程阻塞。

# include<sys/types.h>
# include<sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
参数sockfd:执行过listen系统调用的监听socket
参数addr:用来获取被接受连接的远端socket地址
参数adrlen:socket地址的长度

返回值:成功返回一个新的连接socket,该socket唯一的标识了被接受的这个连接,服务器可以通过读写该socket来与被接受连接对应的客户端通信;失败返回-1

5、发起连接

如果说服务器通过listen调用来被动接受连接,那么客户端需要通过如下系统调用主动与服务器建立连接

# include<sys/types.h>
# include<sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
参数sockfd:由socket系统调用返回一个socket
参数ser_addr:服务器监听的socket地址
参数addrlen:指定服务器监听的socket地址的长度

6、关闭连接

关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述符的系统调用来完成。

# include<unistd.h>
int close(int fd);
fd参数是一个待关闭的socket,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数器减1.
只有当fd的引用计数器为0时,才是真正关闭连接。
多进程中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,
因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。

7、数据接受和发送

对文件的读写操作read和write同样适用于socket,但是socket编程接口提供了几个专门用于socket数据读写的系统调用,以tcp流诗句读写为例:

# include<sys/types.h>
# incldue<sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

四、代码

服务器端:

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

int main()
{
	//创建socket
	//设置网络编程接口
	//AF_INET表示TCP/IPv4协议族
	//SOCK_STREAM表示流式服务
	//0 表示默认协议,几乎所有情况都置0
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	assert(sockfd != -1);
	//定义socket地址类型结构体
	struct sockaddr_in saddr, caddr; //ipv4专用socket地址类型结构

	memset(&saddr, 0, sizeof(saddr));//将socket置空
	//设置socket地址信息
	saddr.sin_family = AF_INET;//指明接口协议族类型
	saddr.sin_port = htons(6000);//指明服务器端口号
	saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//指明服务器IP地址
	                                               //inet_addr函数用于把点分十进制转换为用ipv4网络字节序表示的ipv4地址
	//给创建的socket分配地址,ip地址、端口号等,即将sockfd与saddr绑定
	int res = bind(sockfd,(struct sockaddr*)&saddr, sizeof(saddr));
	assert(res != -1);

	listen(sockfd, 5);//监听socket
						//5表示监听队列最大长度
	while(1)
	{
		int len = sizeof(caddr);
		int c = accept(sockfd, (struct sockaddr*)&caddr, &len);//接收连接  csddr用来存储被接受连接的客户端socket地址
		if(c < 0)
		{
			continue;
		}
		
		char buff[128] = {0};
		recv(c, buff, 127, 0);//读取sockfd上的数据,读取到buff缓冲区中,缓冲区大小为127
		printf("buff = %s\n", buff);
		send(c, "ok", 2, 0);//发送数据给被接受连接端(客户端)

		close(c);//关闭连接
	}
}

客户端:

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

int main()
{
	//创建socket
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	assert(sockfd != -1);
	//设置服务器端socket地址
	struct sockaddr_in saddr;
	memset(&saddr, 0, sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(6000);
	saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	//建立连接
	int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
	assert(res != -1);

	char buff[128] = {0};

	printf("input:\n");
	fgets(buff, 128, stdin);
	//发送消息
	send(sockfd, buff, strlen(buff), 0);
	memset(buff, 0, 128);
	//接收消息
	recv(sockfd, buff, 127, 0);
	printf("buff = %s\n", buff);
	//关闭连接
	close(sockfd);
}

 

 

  • 7
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值