在Windows服务器上搭建代码仓库
一、服务器端
作为服务器端,要想使客户端能够正常访问,必须有自己的地址,使客户端能够在网络中找到该服务器,就相当于在茫茫人海中想和某人交流,必须知道她在哪儿。与此同时,两个人找到了语言不通也无法进行交流,服务器和客户端的进程亦是如此,不使用相同的协议也无法进行通信,因此服务器端需指定所使用的协议。当两个人相遇之后,还有必不可少的一步就是确认身份,在服务器和客户端通信过程中,客户端在网络中找到了服务器端,必须得到服务器端的接受(建立连接)才可进行通信。这一系列操作完成之后两个人方可你一言我一语(接收信息、发送信息)愉快的交流,最后交流结束两人互相告别,即为释放连接的操作。
在客户端寻址服务器的时候还可以进一步进行划分,客户端可以通过ip地址在网络中找到服务器主机的位置,但是在一台主机上可能运行着很多进程,而客户端需要通信的可能只是其中的某一个进程或应用程序,因此还需进一步寻址,找到要建立通信的进程,这就需要知道该进程的端口号,客户端可以通过端口号找到具体的进程。
具体流程如下:
二、客户端
客户端访问服务器,也必须按照服务器的协议进行操作,因此客户端需指定本身所使用的底层协议和服务器相同(即上例中的语言相通)。接下来就是客户端在网络中找服务器,给定客户端服务器的ip地址和具体进程或应用程序的端口号,客户端即可找到服务器。客户端找到服务器之后就申请建立连接,等待服务器接受连接之后,客户端便可与服务器进行通信。最后通信结束关闭连接。
具体流程如下:
三、步骤详解
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:恒置02、命名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_UNIX
AF_UNIX
UNIX本地域协议族
PF_INET
AF_INET
tcp/ipv4协议族
PF_INET6
AF_INET6
tcp/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:指被监听的socket4、接受连接
接受连接是指从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来与被接受连接对应的客户端通信;失败返回-15、发起连接
如果说服务器通过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); }