第1章 理解网络编程和套接字

1.1理解网络编程和套接字

网络编程就是编写程序使两台联网的计算机相互交换数据,除了物理连接外,还需要考虑如何编写数据传输软件。操作系统会提供名为“套接字”的部件。套接字是网络数据传输的软件设备

1.1.1Linux上服务端套接字创建过程函数

1. 生成套接字函数

#include<sys/socket.h>
int socket(int domain,int type, int protocol);
成功时返回文件描述,失败返回-1

2. 给套接字分配地址信息(IP地址和端口地址)

#include<sys/socket.h>
int bind(int socket,struct sockaddr *myaddr,socklen_t addrlen);
成功时返回0,失败时返回-1

3. 把套接字转化成可接受连接的状态

#include<sys/socket.h>
int listen(int sockfd,int backlog);
成功时返回0,失败时返回-1

4. 如果有人为了完成数据传输而请求连接,则用以下函数受理

#include<sys/socket>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
成功时返回文件描述符,失败时返回-1

1.1.2整体流程可整理成

  1. 第一步:调用socket函数创建套接字

  2. 第二步:调用bind函数分配IP地址和端口号

  3. 第三步:调用listen函数转为可接受状态

  4. 第四步:调用accept函数受理连接请求

1.1.3服务端示例流程(以下代码后面篇章会逐步解释,目前只需知道步骤即可)

“Hello World”服务端(文件名hello_server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc,char *argv[])
{
       int serv_sock;
       int clnt_sock;

       struct sockaddr_in serv_addr;
       struct sockaddr_in clnt_addr;
       socklen_t clnt_addr_size;
       char message[] = "Hello World!";

       if(argc!=2)
       {
               printf("Usage : %s <port>\n",argv[0]);
               exit(1);
       }

       serv_sock=socket(PF_INET,SOCK_STREAM,0);//调用socket套接字创建套接字
       if(serv_sock == -1)
               error_handling("sock() error");
       memset(&serv_addr,0,sizeof(serv_addr));
       serv_addr.sin_family=AF_INET;
       serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
       serv_addr.sin_port=htons(atoi(argv[1]));

       if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)//调用bind函数分配IP地址和端口号
               error_handling("bind() error!");
       if(listen(serv_sock,5)==-1)//调用listen函数将套接字转化为可连接状态
               error_handling("listen() error");
       clnt_addr_size=sizeof(clnt_addr);
       clnt_sock=accept((serv_sock),(struct sockaddr*)&clnt_addr,&clnt_addr_size);
       //调用accept函数受理连接请求。如果没有在连接请求的情况下调用函数,则不会返回,直到有连接请求
       if(clnt_sock==-1)
       error_handling("accept() error");

       write(clnt_sock,message,sizeof(message));//write用于传输数据,能够到这一行说明已经有连接请求了
       close(clnt_sock);
       close(serv_sock);
       return 0;
}
void error_handling(char *message)
{
       fputs(message,stderr);
       fputc('\n',stderr);
       exit(1);
}

1.1.4Linux上客户端请求连接套接字创建过程函数

  1. 生成套接字函数与服务端相同

  2. 请求连接函数

#include<sys/socket.h>
int connect(int sockfd,struct sockaddr *ser_addr,socklen_t addrlen);
成功时返回0,失败返回-1

1.1.5客户端示例流程(以下代码后面篇章会逐步解释,目前只需知道步骤即可)

“Hello World”客户端(文件名hello_client.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);

int main(int argc,char* argv[])
{
        int sock;
        struct sockaddr_in serv_addr;
        char message[30];
        int str_len;

        if(argc!=3)
        {
                printf("Usage : %s <IP> <port> \n",argv[0]);
                exit(1);
        }
        //创建套接字,但是不分配IP和端口号。如果紧接调用bind、listen会变成服务端套接字,调用connect函数会成为客户端套接字
        sock=socket(PF_INET,SOCK_STREAM, 0);
        if(sock==-1)
                error_handling("socket() error");

        memset(&serv_addr,0,sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
        serv_addr.sin_port=htons(atoi(argv[2]));

        if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)//调用connect函数向服务端发送连接请求
                error_handling("connect() error!");
        str_len=read(sock,message,sizeof(message)-1);
        if(str_len==-1)
                error_handling("read() error!");

        printf("Message from server : %s \n",message);
        close(sock);
        return 0;
}

1.1.6在Linux环境下运行

gcc hello_server.c -o hserver
./hserver 9190

将前面的文件编译成后面的可执行文件hserver
-o是指定可执行文件名的可选参数
如果执行成功程序应该会停留在此状态,accept未返回

gcc hello_client.c -o hclient
./hclient 127.0.0.1 9190 

如果正确应该是

1.1.7一些补充

服务器创建的套接字称为服务端套接字或监听套接字
对应的就是服务端套接字
127.0.0.1是两个程序在同一台计算机时用的IP,但是如果客户端和服务端不在同一台计算机,应该用服务端IP
Linux中./文件名 可执行当前目录下的文件

1.2基于Linux的文件操作

因为在Linux系统中一切皆文件,socket会被认为成文件的一种,所以可以用文件的一些I/O函数

  1. Linux提供的文件IO函数

文件描述符号

对象

0

标准输入:Standard Input

1

标准输出:Standard Output

2

标准错误:Standard Error

文件和套接字一般在被创建后才分配文件描述符,***输入输出对象即使没经过创建过程,程序运行时也会自动分配文件描述符***
简单理解文件描述符就是方便称呼操作系统创建的文件和套接字赋予的数字
  1. 打开文件函数

   #include<sys/types.h>
   #include<sys/stat.h>
   #include<fcntl.h>
   int open(const char *path,int flag)

成功返回文件描述符,失败返回-1 path 文件名字符串地址 flag 文件打开模式信息 如果需要flag传输多个参数应该用"|"组合并传递

文件打开模式

打开模式

含义

O_CREAT

必要时创建文件

O_TRUNC

删除现在所有数据

O_APPEND

向文件尾追加数据

O_RDONLY

只读打开

O_WRONLY

只写打开

O_RDWR

读写打开

  1. 关闭文件

#include<unistd.h>
int close(int fd);
成功时返回0,失败时返回-1
  1. 将数据写入文件

#include<unistd.h>
ssize_t write(int fd,const void *buf,size_t nbytes);
成功时返回写入的字节数,失败时返回-1
fd  数据传输对象文件描述符
buf 保存要传输数据的缓冲地址
nbytes  要传输数据的字节数
  1. 读取文件中的数据

#include<unistd.h>
ssize_t read(int fd,void * buf,size_t nbytes);
成功返回接收的字节数(读到文件尾返回0),失败返回1
参数与写入函数类似
  1. 一些补充
    前面的size_t和ssize_t是通过typedef声明的无符号整型和有符号整型
    以往的的整型是16位,不同的时代整型的位数可能会改变,提前声明如果在位数改变的情况下,可以减少代码的修改量,不需要把所有代码的整型全部修改,只需要把声明里的即可。由操作系统定义的数据类型加上后缀_t
    如果程序顺序执行打开文件的操作,文件表示符会顺序分配,假如有三个文件或者套接字顺序执行下来就是,3,4,5的文件标识符,0,1,2分配给了前面提到过的标准输入输出和错误

1.3基于Windows的实现

一下均以Visual Stdio 2008为例

  1. 提前准备
    导入头文件winsock2.h
    链接ws2_32.lib库
    大体的流程
    打开该项目属性->链接器->输入->附加依赖项 在附件依赖项添加ws2_32.lib

  2. Winsock初始化

#include<winsock2.h>
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
成功时返回0,失败返回非零错误代码值
Winsock存在多个版本,应该准备WORD(通过typedef声明的短整型)类型的套接字版本信息,并传递给第一个参数。  
如果版本为1.2,其中1是主版本号,2是副版本号,应该传递0x0201  
也就是说,高八位为副版本号,第八位为主版本号,但是手动太麻烦直接用MAKEWORD宏函数轻松构建
MAKEWORD(1,2);//版本号为1.2
第二个参数,传入WSADATA型结构体变量地址LPWSADATA是它的指针类型
  1. Winsock的注销

#include<winsock2.h>
int WSACleanup(void);
成功返回0,失败返回SOCKET_ERROR
  1. 大体流程

int main(){
    WSADATA wsaData;
    ···
    if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
     错误处理
    ···
    ···
    ···
    WSACleanup();
    return 0;
}

1.4基于Windows的套接字相关函数

1. 相关套接字函数

  1. 创建套接字

  #include<winsock2.h>
  SOCKET socket(int af,int type,int protocol);
成功时返回套接字句柄,失败时返回INVALID_SOCKET  
  1. 分配地址

   #include<winsock2.h>
   int bind(SOCKET s,const struct sockaddr * name,int namelen);
    成功返回0,失败返回SOCKET_ERROR
  1. 套接字转化为可接受连接

   #include<winsock2.h>
   int listen(SOCKET s,int backlog);
    成功返回0,失败返回SOCKET_ERROR
  1. 受理客户端连接请求

    #include<winsock2.h>
    SOCKET accept(SOCKET s,struct sockaddr * addr,int * addrlen);
    成功返回套接字句柄,失败返回INVALID_SOCKET
  1. 客户端发出连接请求

    #include<winsock2.h>
    int connect(SOCKET s,const struct sockaddr * name,int namelen);
    成功返回0,失败返回SOCKET_ERROR
  1. 关闭套接字(windows不把socket和文件视为一种)

    #include<winsock2.h>
    int closesocket(SOCKET s);
    成功返回0,失败返回SOCKETERROR

2. Windows中文件句柄和套接字句柄

简单一句:“句柄”实际就是Linux中的文件描述符,只不过要区分socket句柄和文件句柄
笔记就不演示windows的服务器和客户端的例子了,几乎相同流程,只不过要加套接字的初始化和注销

3.基于Windows的IO函数

  1. 发送函数

#include<winsock2.h>
int send(SOCKET s,const char * buf,int len,int flags);
s   数据传输对象连接的套接字句柄
buf 保存待传数据的缓冲地址
len 要传输的字节数
flags   传输数据用到的多种选项信息
  1. 接收函数

#include<winsock2.h>
int recv(SOCKET s,const cahr *buf,int len,int flags)
flags 接收数据时用到的多种选项信息

4.一些补充

之前在Linux环境下用的write和read函数实现的消息发送接收,但是这两个并不对应于Windows的send和recv,后面会介绍到Linux下的对应函数

服务端

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
	WSADATA	wsaData;
	SOCKET hServSock, hClntSock;		
	SOCKADDR_IN servAddr, clntAddr;		

	int szClntAddr;
	char message[]="Hello World!";

	if(argc!=2) 
	{
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
  
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error!"); 
	
	hServSock=socket(PF_INET, SOCK_STREAM, 0);
	if(hServSock==INVALID_SOCKET)
		ErrorHandling("socket() error");
  
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family=AF_INET;
	servAddr.sin_addr.s_addr=htonl(INADDR_ANY);
	servAddr.sin_port=htons(atoi(argv[1]));
	
	if(bind(hServSock, (SOCKADDR*) &servAddr, sizeof(servAddr))==SOCKET_ERROR)
		ErrorHandling("bind() error");  
	
	if(listen(hServSock, 5)==SOCKET_ERROR)
		ErrorHandling("listen() error");

	szClntAddr=sizeof(clntAddr);    	
	hClntSock=accept(hServSock, (SOCKADDR*)&clntAddr,&szClntAddr);
	if(hClntSock==INVALID_SOCKET)
		ErrorHandling("accept() error");  
	
	send(hClntSock, message, sizeof(message), 0);
	closesocket(hClntSock);
	closesocket(hServSock);
	WSACleanup();
	return 0;
}

void ErrorHandling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[30];
	int strLen;

	if(argc!=3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");  
	
	hSocket=socket(PF_INET, SOCK_STREAM, 0);
	if(hSocket==INVALID_SOCKET)
		ErrorHandling("socket() error");
	
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family=AF_INET;
	servAddr.sin_addr.s_addr=inet_addr(argv[1]);
	servAddr.sin_port=htons(atoi(argv[2]));
	
	if(connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr))==SOCKET_ERROR)
		ErrorHandling("connect() error!");
 
	strLen=recv(hSocket, message, sizeof(message)-1, 0);
	if(strLen==-1)
		ErrorHandling("read() error!");
	printf("Message from server: %s \n", message);  

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

本文章使用limfx的vscode插件快速发布