TCP/IP网络编程 学习笔记_1 --网络编程入门

时间:2021-08-03 09:54:45

前言:这个系列网络编程教程实例代码是在Xcode上运行的,MacOSX,是一个基于UNIX核心的系统,所以基于Linux的网络编程代码一般可以直接在Xcode上运行,如果要移植到Windows其实就只需要稍微改下,本章下面有讲Windows上的不同之处。

网络编程和套接字

  • 网络编程其实和我们计算机上的文件读取操作很类似,通俗地讲,网络编程就是编写程序使两台联网的计算机相互交换数据。那么,数据具体怎么传输呢?其实操作系统会提供名为“套接字”的部件,套接字就是网络数据传输用的软件设备而已。即使你对网络数据传输原理不太熟悉,你也可以通过套接字完成数据传输。因此,网络编程常常又称为套接字编程。

  • 下面我们再通过一个通俗地例子来理解什么是套接字并给出创建它的过程。实际上,这个过程类似我们的电话机系统,电话机通过固定电话网完成语言数据的交换。这里的电话机就类似我们的套接字,电网就类似我们的互联网。和电话可以拨打或接听一样,套接字也可以发送或接收。先来看看接收的套接字创建过程:
    1,打电话首先需要准备什么?当然得是要先有一台电话机。创建相当于电话机的套接字,如下:

    int socket(int domain, int type, int protocol);

    2,准备好电话机后要考虑分配电话号码的问题,这样别人才能联系到你。套接字也一样,利用下面函数创建好套接字分配地址信息(IP地址和端口号)。

    int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);

    3,做了上面两步后,接下来就是需要连接电话线并等待来电。一连接电话线,电话机就转为了可接听状态,这时其他人可以拨打电话请求连接到该机了。同样,需要把套接字转化成可接收连接的状态。

    int listen(int sockfd, int backlog);

    4,前面都做好后,如果有人拨打电话就会响铃,拿起话筒才能接听电话。套接字同样如此,如果有人为了完成数据传输而请求连接,就需要调用下面函数进行受理。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

  • 总结下网络中接收连接请求的套接字创建过程如下:
    第一步:调用socket函数创建套接字。
    第二步:调用bind函数分配IP地址和端口号。
    第三部:调用listen函数转为可接收请求状态。
    第四步:调用accept函数受理连接请求。

  • 上面讲的都是接电话,即服务端套接字(接收),下面我们再来讲讲打电话,即客服端套接字(发送)。这个要简单,只有两步:1,调用socket函数创建套接字。2,调用connect函数向服务端发送连接请求。

    int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);

基于Linux的文件操作

1,在这里为什么要讨论Linux上的文件操作呢?因为Linux上,socket操作与文件操作没有区别,在Linux上,socket也被认为是文件的一种。
注:Linux上的C语言编译器–GCC,具体使用就不在这里讲了。

2,文件描述符:是系统自动分配给文件或套接字的整数。下面我们再来通过一个例子理解下它:假设学校有个打印室,只需要打个电话就能复印所需论文。有一位同学,经常打电话要复印这样个内容:“<<关于随着高度信息化社会而逐渐提升地位的触觉,知觉,思维,性格,智力等人类生活质量相关问题特性的人类学研究>>这篇论文第26页到30页”。终于有一天,打印室的人感觉这样太不方便了,于是,打印室的人和那位同学说:“以后那篇论文就编为第18号,你就说帮我复印18号论文26页到30页”。在该例中,打印室相当于操作系统,那位同学相当于程序员,论文号相当于文件描述符,论文相当于文件或套接字。也就是说,每当生成文件或套接字,操作系统就会自动返回给我们一个整数。这个整数就是文件描述符,即创建的文件或套接字的别名,方便称呼而已。
注:文件描述符在Windows中又称为句柄。

3,Linux上的文件或套接字操作:
打开文件:

int open(const char *path, int flag); –> (Linux上对应socket(…)函数)

关闭文件或套接字:

int close(int fd); –>(Windows上对应closesocket(SOCKET S)函数)

将数据写入文件或传递数据:

ssize_t write(int fd, const void *buf, size_t nbytes);

读取文件中数据或接收数据:

ssize_t read(int fd, void *buf, size_t nbytes);

注释:ssize_t = signed int, size_t = unsigned int,其实它们都是通过typedef声明的,为基本数据类型取的别名而已。既然已经有了基本数据类型,那么为什么还需要为它取别名呢?是因为目前普遍认为int是32位的,而过去16位操作系统时代,int是16位的。根据系统的不同,时代的变化,基本数据类型的表现形式也随着变化的。如果为基本数据类型取了别名,以后要修改,也就只需要修改typedef声明即可,这将大大减少代码变动。

基于Windows平台的实现

1,Windows套接字大部分是参考BSD系列UNIX套接字设计的,所以很多地方都跟Linux套接字类似。因此,只需要更改Linux环境下编好的一部分网络程序内容,就能再Windows平台下运行。

2,上面讲了Linux上,文件操作和套接字操作一致。但Windows上的I/O函数和套接字I/O函数是不同的。
Winsock数据传输函数:

int send(SOCKET s, const char *buf, int len, int flags);

Winsock数据接收函数:

int recv(SOCKET s, const char *buf, int len, int flags);

3,Windows与Linux上的套接字再一个区别是:Windows上需要先对Winsock库进行初始化,最后退出还要注销Winsock相关库。

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
第一个参数:Winsock中存在多个版本,应准备WORD类型的(WORD是typedef声明的unsigned short)套接字版本信息。若版本为1.2,则其中1是主版本号,2是副版本号,应传递0x0201。高8位为副版本号,低8位为主版本号。我们还可以直接使用宏,MAKEWORD(1,2); //主版本号为1,副版本为2,返回0x0201。
第二个参数:就是传入WSADATA型结构体变量地址。

Winsock库初始化:

int main(int  argc, char *argv[])
{
WSADATA wsaData;
...
if(WSAStartup(MAKEWORD(1,2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
...
return 0;
}

在退出时需要释放Winsock库:

int WSACleanup(void); //返回0成功,失败返回SOCKET_ERROR

Linux上代码实例

  • 服务端:
//
// main.cpp
// hello_server
//
// Created by app05 on 15-7-6.
// Copyright (c) 2015年 app05. All rights reserved.
//

/*
1,argv[]默认它只有一个参数就是程序名,那么怎么手动给它添加参数呢?首先要清楚
给它添加参数是为了用命令行参数就算以后要修改这个参数就只要编译器工具里设置
下就可以了而不需要改代码,如端口设置。
2,在Xcode中给程序提供命令行参数步骤:Product菜单-->Scheme-->Edit Scheme-->Run-->Arguments-->+添加
*/


#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)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}


int main(int argc, const 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);
if(serv_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 = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));

if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");

if(listen(serv_sock, 5) == -1)
error_handling("listen() error");

clnt_addr_size = sizeof(clnt_addr);
//如果没有收到请求,则不返回,只到有连接请求为止
clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
if(clnt_sock == -1)
error_handling("accept() error");

write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);

return 0;
}
  • 客服端:
//
// main.cpp
// hello_client
//
// Created by app05 on 15-7-6.
// Copyright (c) 2015年 app05. All rights reserved.
//

/*
1,如果服务端和客服端是同一台电脑,那么IP地址可以填:127.0.0.1,
如果不在同一台电脑上,则应该填服务端电脑IP。
2,运行时,先运行服务端程序,然后再运行客服端程序。
*/


#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)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

int main(int argc, const 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);
}

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)
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;
}

TCP/IP网络编程 学习笔记_1 --网络编程入门