socket网络编程基础篇

本文深入解析Socket网络通信原理,涵盖服务器与客户端建立连接的过程、关键函数的使用方法及实例代码。详细介绍了socket创建、绑定、监听、接受连接及数据收发等核心步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

         首先列举一下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、int listenintsocketfdintbacklog);

//此函数调用后,当客户端调用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 8888

int 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位为保留位,清0


	iRet = 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;
}







一个简单的socket网络编程例子: 服务器代码: #include #include #include #include #pragma comment(lib,"ws2_32.lib") //这句话的意思是加载ws2_32.lib这个静态库 #define NETWORK_EVENT WM_USER+100 //如果你用mfc做开发,你可以点击菜单project-〉setting-〉link-〉object/library中添加这个静态库。 //如果你用c语言,你需要通过#pragma comment(命令来连接静态库 int main(int argc, char* argv[]){ HANDLE hThread = NULL; //判断是否输入了端口号 if(argc!=3){ printf("Usage: %sPortNumber\n",argv[1]); exit(-1); } //把端口号转化成整数 short port; if((port = atoi(argv[2]))==0){ printf("端口号有误!"); exit(-1); } WSADATA wsa; //初始化套接字DLL if(WSAStartup(MAKEWORD(2,2),&wsa)!=0){ //高字节指定了次版本号,低字节指定了主版本号,两个字节加到一起,就是你想要的Winsock库的版本号了 printf("套接字初始化失败!"); exit(-1); } //创建套接字 SOCKET serverSocket; if((serverSocket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))==INVALID_SOCKET){ printf("创建套接字失败!"); exit(-1); } struct sockaddr_in serverAddress; memset(&serverAddress,0,sizeof(sockaddr_in)); serverAddress.sin_family=AF_INET; serverAddress.sin_addr.S_un.S_addr = htonl(INADDR_ANY); serverAddress.sin_port = htons(port); //绑定 if(bind(serverSocket,(sockaddr*)&serverAddress,sizeof(serverAddress))==SOCKET_ERROR){ printf("套接字绑定到端口失败!端口: %d\n",port); exit(-1); } //进入侦听状态 if(listen(serverSocket,SOMAXCONN)==SOCKET_ERROR){ printf("侦听失败!"); exit(-1); } printf("Server %d is listening......\n",port); SOCKET clientSocket[5],maxSocket;//用来和客户端通信的套接字 struct sockaddr_in clientAddress;//用来和客户端通信的套接字地址 memset(&clientAddress,0,sizeof(clientAddress)); int addrlen = sizeof(clientAddress); fd_set fd_read; int i=0; int j; char buf[4096]; char buff[4096]="exit"; while(1) { FD_ZERO(&fd_read); maxSocket=serverSocket; FD_SET(serverSocket,&fd_read); //FD_SET(clientSocket[i-1],&fd_read); for(j=0;j<i;j++) { FD_SET(clientSocket[j],&fd_read); if(maxSocket"); //gets(buff); if(select(maxSocket+1,&fd_read,NULL,NULL,NULL)>0) { if(FD_ISSET(serverSocket,&fd_read)) { if(buff=="") { if((clientSocket[i++]=accept(serverSocket,(sockaddr*)&clientAddress,&addrlen))==INVALID_SOCKET) { printf("接受客户端连接失败!"); exit(-1); } else { for(j=0;j5) { printf("超过最大客户端数"); exit(-1); } } else { int bytes; for(int k=0;k<i;k++) { if(FD_ISSET(clientSocket[k],&fd_read)) { bytes=recv(clientSocket[k],buf,sizeof(buf),0); if(bytes==-1) { //listen(serverSocket,SOMAXCONN); for (int l=k;l<i;l++) clientSocket[l]=clientSocket[l+1]; i--; } /*if(bytes==0) { //printf("fdsdf"); listen(serverSocket,SOMAXCONN); for (int l=k;l0) { buf[bytes]='\0'; printf("Message from %s: %s\n",inet_ntoa(clientAddress.sin_addr),buf); if(send(clientSocket[k],buf,bytes,0)==SOCKET_ERROR) { printf("发送数据失败!"); exit(-1); } } } } } } } //清理套接字占用的资源 WSACleanup(); return 0; } 客户端代码: #include #include #include #pragma comment(lib,"ws2_32.lib") int main(int argc, char* argv[]){ //判断是否输入了IP地址和端口号 if(argc!=4){ printf("Usage: %s IPAddress PortNumber\n",argv[1]); exit(-1); } //把字符串的IP地址转化为u_long unsigned long ip; if((ip=inet_addr(argv[2]))==INADDR_NONE){ printf("不合法的IP地址:%s",argv[1]); exit(-1); } //把端口号转化成整数 short port; if((port = atoi(argv[3]))==0){ printf("端口号有误!"); exit(-1); } printf("Connecting to %s:%d......\n",inet_ntoa(*(in_addr*)&ip),port); WSADATA wsa; //初始化套接字DLL if(WSAStartup(MAKEWORD(2,2),&wsa)!=0){ printf("套接字初始化失败!"); exit(-1); } //创建套接字 SOCKET sock,serverSocket; if((sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))==INVALID_SOCKET){ printf("创建套接字失败!"); exit(-1); } struct sockaddr_in serverAddress; memset(&serverAddress,0,sizeof(sockaddr_in)); serverAddress.sin_family=AF_INET; serverAddress.sin_addr.S_un.S_addr = ip; serverAddress.sin_port = htons(port); //建立和服务器的连接 if(connect(sock,(sockaddr*)&serverAddress,sizeof(serverAddress))==SOCKET_ERROR) { printf("建立连接失败!"); exit(-1); } char buf[4096]; while(1){ printf(">"); //从控制台读取一行数据 gets(buf); if(send(sock,buf,strlen(buf),0)==SOCKET_ERROR){ printf("发送c数据失败!"); exit(-1); } int bytes; if((bytes=recv(sock,buf,sizeof(buf),0))==SOCKET_ERROR) { printf("接收c数据失败!\n"); exit(-1); } else { buf[bytes]='\0'; printf("Message from %s: %s\n",inet_ntoa(serverAddress.sin_addr),buf); } } //清理套接字占用的资源 WSACleanup(); return 0; }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值