socket网络编程基础篇

来源:互联网 发布:软件项目考核指标 编辑:程序博客网 时间:2024/05/01 13:13

         首先列举一下socket网络通信的例子:使用局域网打游戏,用浏览器连接外网看视频,使用QQ与好友通信,手机连接wifi传数据等等。socket是底层抽象给应用层所使用的一套接口函数,本篇讲解这些函数的使用。


对象:1、服务器server(等待客户端连接)

           2、客户端client(主动连接服务器)

对象之间的联系:

        client是根据server‘’ip地址+端口号”找到对方并建立连接的

        1、ip地址:不用说了,就是192.168.6.xxx之类(一个主机可能有多个ip)。

        2、端口:同一个ip下又可分为多个端口,做个比喻吧:ip相当于一个大别墅,多个端口相当于

            别墅里的多个房间,数据就相当于客人,客人可以进不同的房间干不同的事情(即业务)。

传输方式:

  1、TCP(数据可靠,一般常用这种)

  2、UDP(数据不可靠,一般用于实时视频传输)


server(服务器必要代码)

 

1、fd = socket(int domain, int type, int protocol);

//相当于获得了一个标志(fd就是这个服务器了),以后想用这个服务器就去找fd就行了

●domain:协议域或协议族,例如AF_INET、AF_INET6、AF_LOCAL等,其决定了socket的地址类型,例如我们常用的AF_INET决定了要用ipv4地址(32位)+端口号(16位)的组合。

●type:指定socket类型,常用的有SOCK_STREAM、SOCK_DGRAM、SOCK_RAM等等

●protocal:指定协议,TCP协议、UDP协议、STCP协议、TIPC协议

//注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。设置protocol为0时,会自动选择type类型对应的默认协议。

 

2、int blind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

//blind翻译为绑定,就是将上面socket()出来的标志fd与真实服务器的地址进行绑定,因为人家是要连接这个地址,绑定后fd才真正的成为了这个地址(服务器)的代言人!

sockfd:就是那个fd(服务器的代言人)

addr:要绑的地址(服务器的ip和端口),所以在调用blind函数之前需要先设置这个结构体

            要注意的是这个地址根据创建socket时的协议族的不同而不同

  小技巧:man 7ip迅速查找到并粘贴出来

  //对应ipv4格式的地址如下所示:

 struct sockaddr_in {  sa_family_t sin_family; /* address family: AF_INET */  in_port_t sin_port; /* port in network byte order */  struct in_addr sin_addr; /* internet address */  };   /* Internet address. */  struct in_addr {  uint32_t s_addr; /* address in network byte order */  };

//注意:这里发现blind函数的参数2是sockaddr结构,但是ipv4的是sockaddr_in结构,所以要做一个强制类型转换成通用的sockaddr结构(其他ipv6等等也都要这样做)

addrlen:对应地址的长度

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

//注意:这个函数是服务器独有的,客户端不需要,因为客户端在调用connect函数的时候系统会自动分配一个本机ip+端口给他。

 

3、intlistenintsocketfdintbacklog);

//此函数调用后,当客户端调用connect函数发出连接请求时,服务器端会收到此请求。

//且listen函数一旦调用,此fd将变成被动套接字(今后只能等待别人来连接,而不能主动连)

//内部维护了两个队列:1、已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手

                                          2、已完成连接的队列

//后续调用的accept函数(继续往后看)会从第二个队列中取出一个连接

●socketfd就是那个fd(服务器的代言人)

●backlog排队的最大连接个数

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

 

4、int accept(int sockfd, structsockaddr *addr, socklen_t *addrlen);

//从已完成连接队列(即listen内部维护的队列)返回第一个连接,如果已完成连接队列为空,则阻塞。

sockfd:就是那个fd(服务器的代言人)

addr:获得对方的地址存在此结构中(客户端的地址)

addrlen:地址长度

返回值:成功返回客户端的fd(客户端代言人),失败返回-1

 

5、read()/write() 或者 recv()/send()

  ssize_tread(intfd,void *buf,size_tcount);

  ssize_twrite(intfd,void *buf,size_tcount);

  ssize_trecv(intsockfd,void *buf,size_tlen,intflags);

  ssize_tsend(intsockfd,constvoid *buf,size_tlen,intflags);

//共同点:这两套读写函数都可以实现数据的收发。

//区别:1、read函数可用于文件/套接字/标准输入输出,而recv只能用于套接字

// 2、recv()函数多了个参数flag;//flag可取值:MSG_OOB(带外数据 紧急指针)

// MSG_PEEK(数据包的提前预读)

// flag取0则等同于read函数

 

client(客户端必要代码)

1、fd = socket();//获得客户端代言人fd

//函数讲解、函数参数同server,略。

 

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

//将客户端连接到服务器,调用connect函数后服务器的accept函数会收到这个连接的

sockfd:就是那个fd(客户端的代言人)

addr:要连接的服务器的地址(在调用connect之前要填充这个地址的结构体!)

addrlen:地址长度

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

 

3、read()/write() 或者 recv()/send()

//函数讲解、函数参数同server,略。

 

其他代码

1、字节序转换代码:

问:字节序是什么?为什么要转换字节序?

答:由于进行网络传输的双方不一定在同一个主机上,可能是PC----PC

或者PC----ARM...等等不同架构之间通信,而存在大端和小端的说法。

1、大端:低位存放于高内存地址处

2、小端:低位存放于低内存地址处

测试自己主机是大端还是小端方法:

void main(){  unsigned int data = 0x12345678;  char *p = &data;  printf(“%x %x %x %x \n”,p[0],p[1],p[2],p[3]);  if(p[0] == 0x78)  {  printf(“当前系统是小端模式”);  }  else  {  printf(“当前系统是大端模式”);  }}

 

在x86下测试打印得出:

 78 56 34 12

 当前系统是小端模式

在大端(Big Endian)和小端(Little Endian)中,数据在内存中的存放顺序是不一样的,例如在大端的A主机上的内存里一个数据,将其发送给小端的B主机,存在内存中顺序就错了,这对socket传输是致命的,所以需要解决这种问题:

解决方法:

1、在发送之前先转换成网络字节序(网络字节序为大端)

2、然后接收端收到的是网络字节序

3、接收端将网络字节序转换成本地字节序即可(不同主机不同,例:x86位小端、ARM可配置)

引出了了一系列字节序转换函数:

uint32_t htonl(uint32_t hostlong);//主机字节序转为网络字节序

uint16_t htons(uint16_t hostshort);//主机字节序转为网络字节序

uint32_t ntohl(uint32_t netlong);//网络字节序转为主机字节序

uint16_t ntohs(uint16_t netshort);//网络字节序转为主机字节序

说明:在上述的函数中,h代表host主机;n代表network s代表short;l代表longs代表short

2、地址转换代码:

问:为什么需要地址转换?

答:比如客户端要连接服务器的ip是192.168.6.112,客户端要先把这个ip转换为一个32位的 ipv4然后再去连接

引出了了一系列地址转换函数:

int inet_aton(const char *cp, struct in_addr *inp);//192.168.6.xx====>in_addr结构

in_addr_t inet_addr(const char *cp); //192.168.6.xx====>in_addr结构

char *inet_ntoa(struct in_addr in); //in_addr结构====>192.168.6.xx

 

到此基础讲解完毕,下面贴代码

代码功能:

         客户端从控制台键盘输入并发送给服务器,服务器收到数据并打印出客户端信息和数据。

         服务器端用了fork,即可支持多客户端连接。

server.c:

 

#include <sys/types.h>         #include <sys/socket.h>#include <string.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <stdio.h>#include <signal.h>#define SERVER_PORT 8888 //端口号,定义为宏方便以后直接修改#define BACKLOG     10   //表服务器可以同时监测多少个客户端连接,设置为>0即可int main(int argc, char **argv){int iSocketServer;int iSocketClient;struct sockaddr_in tSocketServerAddr;//服务器地址结构struct sockaddr_in tSocketClientAddr;//客户端地址结构:后来当客户端来连接时会传过来int iRet;int iAddrLen;int iRecvLen;unsigned char ucRecvBuf[1000];//服务器收到数据的缓冲区int iClientNum = -1;/*  *防止僵尸进程,子进程结束后还是会存于进程表项中,可用ps -u book(用户)查看到 *所以要发送一个信号SIGCHLD给父进程,让其给它收尸(注:所有64个信号可由kill -l查看) *SIG_IGN为忽略的意思,可让内核把僵尸进程转交给init进程去处理,防止其占用系统资源 */signal(SIGCHLD,SIG_IGN);/*防止僵尸进程:子进程退出后会给父进程一个信号,然后来给它收尸即可*/iSocketServer = socket(AF_INET, SOCK_STREAM, 0);if (-1 == iSocketServer){printf("socket error!\n");return -1;}tSocketServerAddr.sin_family      = AF_INET;tSocketServerAddr.sin_port        = htons(SERVER_PORT);  /* 端口是2个字节即short型 */ tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;/*本机上所有IP,若为特定ip的话需要用inet_addr()函数来转换一下*/memset(tSocketServerAddr.sin_zero, 0, 8);/*设置为0----8字节*/iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));/* 注:上面函数第二个参数强制类型转换为通用的sockaddr结构(因为sockaddr_in结构是)  */if (-1 == iRet){printf("bind error!\n");return -1;}  iRet = listen(iSocketServer, BACKLOG);if (-1 == iRet){printf("listen error!\n");return -1;}while (1){iAddrLen = sizeof(struct sockaddr);iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);/* 参数2获得了对方的IP地址  */if (-1 != iSocketClient){iClientNum++;printf("Get connect from client %d : %s\n",  iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));/*  *在父进程中调用fork()返回子进程的PID号,取非则变为0,所以直接跳过if,转到while开头继续accept新的客户端 *而子进程的fork返回0,则继续进去执行 */if (!fork()){/* 子进程 */while (1){/* 接收客户端发来的数据并显示出来 */iRecvLen = recv(iSocketClient, ucRecvBuf, 999, 0);if (iRecvLen <= 0){//如果在读的过程中对方关闭,tcpip协议会返回一个0数据包close(iSocketClient);return -1;}else{ucRecvBuf[iRecvLen] = '\0';printf("Get Msg From Client %d: %s\n", iClientNum, ucRecvBuf);}}}}}close(iSocketServer);return 0;}

client.c:

#include <sys/types.h>        #include <sys/socket.h>#include <string.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <stdio.h>#define SERVER_PORT 8888int main(int argc, char **argv){int iSocketClient;struct sockaddr_in tSocketServerAddr;int iRet;unsigned char ucSendBuf[1000];//发送缓冲区int iSendLen;if (argc != 2){printf("Usage:\n");printf("%s <server_ip>\n", argv[0]);//参数不为2个就打印用法return -1;}iSocketClient = socket(AF_INET, SOCK_STREAM, 0);tSocketServerAddr.sin_family      = AF_INET;tSocketServerAddr.sin_port        = htons(SERVER_PORT); if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr)) {printf("invalid server_ip\n");return -1;}memset(tSocketServerAddr.sin_zero, 0, 8);//结构体后8位为保留位,清0iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));if (-1 == iRet){printf("connect error!\n");return -1;}while (1){if (fgets(ucSendBuf, 999, stdin))/*从stdin获得数据(我们自己实时敲入的)到ucSendBuf*/{iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0);if (iSendLen <= 0){close(iSocketClient);return -1;}}}return 0;}






0 0